- Inject bonfire.Output from stdin/stdout/stderr with stdlib widgets and x/term - Degrade spinner, progress, table and prompts without a TTY or color - Reject plugin command names that are not namespace:verb
171 lines
3.4 KiB
Go
171 lines
3.4 KiB
Go
package bonfire
|
||
|
||
import (
|
||
"fmt"
|
||
"io"
|
||
"os"
|
||
|
||
"golang.org/x/term"
|
||
)
|
||
|
||
const (
|
||
ansiReset = "\x1b[0m"
|
||
ansiRed = "\x1b[31m"
|
||
ansiGreen = "\x1b[32m"
|
||
ansiYellow = "\x1b[33m"
|
||
ansiCyan = "\x1b[36m"
|
||
ansiDim = "\x1b[90m"
|
||
|
||
symCheck = "✓"
|
||
symCross = "✗"
|
||
symWarn = "⚠"
|
||
symInfo = "ℹ"
|
||
symArrow = "→"
|
||
symPrompt = "?"
|
||
)
|
||
|
||
// Output is the injected console used by commands. Tests supply streams.
|
||
type Output interface {
|
||
io.Writer
|
||
Println(a ...any)
|
||
Printf(format string, a ...any)
|
||
Info(message string)
|
||
Success(message string)
|
||
Error(message string)
|
||
Warning(message string)
|
||
Table(headers []string, rows [][]string)
|
||
Spinner(message string, fn func() error) error
|
||
Progress(total int, fn func(Progress) error) error
|
||
Ask(question, def string) (string, error)
|
||
Confirm(question string, def bool) (bool, error)
|
||
Choice(question string, choices []string, def int) (string, error)
|
||
Secret(question string) (string, error)
|
||
}
|
||
|
||
// Progress is a single progress bar advanced from a command.
|
||
type Progress interface {
|
||
Advance(step int)
|
||
}
|
||
|
||
type console struct {
|
||
in io.Reader
|
||
out io.Writer
|
||
err io.Writer
|
||
lookup func(string) string
|
||
tty bool
|
||
interactive bool
|
||
color bool
|
||
}
|
||
|
||
// NewOutput builds Output from injected streams and the process color policy.
|
||
func NewOutput(in io.Reader, out, errW io.Writer) Output {
|
||
return newConsole(in, out, errW, os.Getenv, nil, nil)
|
||
}
|
||
|
||
func newConsole(in io.Reader, out, errW io.Writer, lookup func(string) string, tty, interactive *bool) *console {
|
||
if in == nil {
|
||
in = os.Stdin
|
||
}
|
||
if out == nil {
|
||
out = os.Stdout
|
||
}
|
||
if errW == nil {
|
||
errW = out
|
||
}
|
||
if lookup == nil {
|
||
lookup = os.Getenv
|
||
}
|
||
c := &console{in: in, out: out, err: errW, lookup: lookup}
|
||
if tty != nil {
|
||
c.tty = *tty
|
||
} else {
|
||
c.tty = isTerminal(out)
|
||
}
|
||
if interactive != nil {
|
||
c.interactive = *interactive
|
||
} else {
|
||
c.interactive = isTerminal(in) && c.tty
|
||
}
|
||
c.color = supportsColor(lookup, c.tty)
|
||
return c
|
||
}
|
||
|
||
func supportsColor(lookup func(string) string, tty bool) bool {
|
||
if lookup == nil {
|
||
lookup = os.Getenv
|
||
}
|
||
if lookup("NO_COLOR") != "" || lookup("TERM") == "dumb" {
|
||
return false
|
||
}
|
||
if lookup("FORCE_COLOR") != "" {
|
||
return true
|
||
}
|
||
return tty
|
||
}
|
||
|
||
func isTerminal(rw any) bool {
|
||
type fd interface{ Fd() uintptr }
|
||
f, ok := rw.(fd)
|
||
if !ok {
|
||
return false
|
||
}
|
||
return term.IsTerminal(int(f.Fd()))
|
||
}
|
||
|
||
func (c *console) Write(p []byte) (int, error) {
|
||
if c.out == nil {
|
||
return 0, io.ErrClosedPipe
|
||
}
|
||
return c.out.Write(p)
|
||
}
|
||
|
||
func (c *console) Println(a ...any) {
|
||
if c.out == nil {
|
||
return
|
||
}
|
||
fmt.Fprintln(c.out, a...)
|
||
}
|
||
|
||
func (c *console) Printf(format string, a ...any) {
|
||
if c.out == nil {
|
||
return
|
||
}
|
||
fmt.Fprintf(c.out, format, a...)
|
||
}
|
||
|
||
func (c *console) Info(message string) {
|
||
c.Println(c.style(ansiCyan, symInfo) + " " + message)
|
||
}
|
||
|
||
func (c *console) Success(message string) {
|
||
c.Println(c.style(ansiGreen, symCheck) + " " + message)
|
||
}
|
||
|
||
func (c *console) Error(message string) {
|
||
w := c.err
|
||
if w == nil {
|
||
w = c.out
|
||
}
|
||
if w == nil {
|
||
return
|
||
}
|
||
fmt.Fprintln(w, c.style(ansiRed, symCross)+" "+message)
|
||
}
|
||
|
||
func (c *console) Warning(message string) {
|
||
c.Println(c.style(ansiYellow, symWarn) + " " + message)
|
||
}
|
||
|
||
func (c *console) style(code, s string) string {
|
||
if !c.color {
|
||
return s
|
||
}
|
||
return code + s + ansiReset
|
||
}
|
||
|
||
func (c *console) dim(s string) string {
|
||
return c.style(ansiDim, s)
|
||
}
|
||
|
||
func boolPtr(v bool) *bool { return &v }
|