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 }