mirror of https://github.com/knative/func.git
255 lines
6.5 KiB
Go
255 lines
6.5 KiB
Go
package progress
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"runtime"
|
|
"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
|
|
|
|
// print N/M step counter with messages
|
|
printWithStepCounter 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
|
|
}
|
|
}
|
|
|
|
// 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 WithPrintStepCounter(s bool) Option {
|
|
return func(b *Bar) {
|
|
b.printWithStepCounter = s
|
|
}
|
|
}
|
|
|
|
func New(verbose bool, options ...Option) *Bar {
|
|
b := &Bar{
|
|
out: os.Stdout,
|
|
verbose: verbose,
|
|
}
|
|
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
|
|
}
|
|
|
|
// Start the spinner if not already started
|
|
if b.ticker == nil {
|
|
fmt.Println()
|
|
b.ticker = time.NewTicker(100 * time.Millisecond)
|
|
go b.spin(b.ticker.C)
|
|
}
|
|
|
|
// Otherwise we are in non-verbose, interactive mode. Do a line-overwrite.
|
|
b.overwrite(" ") // Write with space for the spinner
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// Stopping indicates the process is stopping, such as having received a context
|
|
// cancellation.
|
|
func (b *Bar) Stopping() {
|
|
// currently stopping is equivalent in effect to Done
|
|
b.Done()
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|
|
|
|
// Write a simple line status update.
|
|
func (b *Bar) write() {
|
|
fmt.Fprintln(b.out, b)
|
|
}
|
|
|
|
// 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 messages print correctly.
|
|
if runtime.GOOS == "windows" {
|
|
fmt.Fprintf(b.out, "\r%v%v\n", prefix, b)
|
|
} else {
|
|
fmt.Fprintf(b.out, "\r%v%v%v%v\n", up, clear, prefix, b)
|
|
}
|
|
}
|
|
|
|
func (b *Bar) String() string {
|
|
if b.printWithStepCounter {
|
|
return fmt.Sprintf("%v/%v %v", b.index, b.total, b.text)
|
|
}
|
|
return b.text
|
|
}
|
|
|
|
// Write a spinner at the beginning of the previous line.
|
|
func (b *Bar) spin(ch <-chan time.Time) {
|
|
if b.verbose {
|
|
return
|
|
}
|
|
// Various options for spinners.
|
|
// spinner := []string{"|", "/", "-", "\\"}
|
|
// spinner := []string{"◢", "◣", "◤", "◥"}
|
|
spinner := []string{
|
|
"🕛 ",
|
|
"🕐 ",
|
|
"🕑 ",
|
|
"🕒 ",
|
|
"🕓 ",
|
|
"🕔 ",
|
|
"🕕 ",
|
|
"🕖 ",
|
|
"🕗 ",
|
|
"🕘 ",
|
|
"🕙 ",
|
|
"🕚 ",
|
|
}
|
|
if runtime.GOOS == "windows" {
|
|
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.
|
|
if runtime.GOOS == "windows" {
|
|
fmt.Fprintf(b.out, "\r%v\r", spinner[idx])
|
|
} else {
|
|
fmt.Fprintf(b.out, "\r%v%v%v\r", up, spinner[idx], down)
|
|
}
|
|
idx = (idx + 1) % len(spinner)
|
|
}
|
|
}
|