cli/pkg/iostreams/iostreams.go
2020-09-10 12:13:48 -05:00

197 lines
3.8 KiB
Go

package iostreams
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"strconv"
"strings"
"time"
"github.com/briandowns/spinner"
"github.com/mattn/go-colorable"
"github.com/mattn/go-isatty"
"golang.org/x/crypto/ssh/terminal"
)
type IOStreams struct {
In io.ReadCloser
Out io.Writer
ErrOut io.Writer
// the original (non-colorable) output stream
originalOut io.Writer
colorEnabled bool
progressIndicatorEnabled bool
progressIndicator *spinner.Spinner
stdinTTYOverride bool
stdinIsTTY bool
stdoutTTYOverride bool
stdoutIsTTY bool
stderrTTYOverride bool
stderrIsTTY bool
neverPrompt bool
}
func (s *IOStreams) ColorEnabled() bool {
return s.colorEnabled
}
func (s *IOStreams) SetStdinTTY(isTTY bool) {
s.stdinTTYOverride = true
s.stdinIsTTY = isTTY
}
func (s *IOStreams) IsStdinTTY() bool {
if s.stdinTTYOverride {
return s.stdinIsTTY
}
if stdin, ok := s.In.(*os.File); ok {
return isTerminal(stdin)
}
return false
}
func (s *IOStreams) SetStdoutTTY(isTTY bool) {
s.stdoutTTYOverride = true
s.stdoutIsTTY = isTTY
}
func (s *IOStreams) IsStdoutTTY() bool {
if s.stdoutTTYOverride {
return s.stdoutIsTTY
}
if stdout, ok := s.Out.(*os.File); ok {
return isTerminal(stdout)
}
return false
}
func (s *IOStreams) SetStderrTTY(isTTY bool) {
s.stderrTTYOverride = true
s.stderrIsTTY = isTTY
}
func (s *IOStreams) IsStderrTTY() bool {
if s.stderrTTYOverride {
return s.stderrIsTTY
}
if stderr, ok := s.ErrOut.(*os.File); ok {
return isTerminal(stderr)
}
return false
}
func (s *IOStreams) CanPrompt() bool {
if s.neverPrompt {
return false
}
return s.IsStdinTTY() && s.IsStdoutTTY()
}
func (s *IOStreams) SetNeverPrompt(v bool) {
s.neverPrompt = v
}
func (s *IOStreams) StartProgressIndicator() {
if !s.progressIndicatorEnabled {
return
}
sp := spinner.New(spinner.CharSets[11], 400*time.Millisecond, spinner.WithWriter(s.ErrOut))
sp.Start()
s.progressIndicator = sp
}
func (s *IOStreams) StopProgressIndicator() {
if s.progressIndicator == nil {
return
}
s.progressIndicator.Stop()
s.progressIndicator = nil
}
func (s *IOStreams) TerminalWidth() int {
defaultWidth := 80
out := s.Out
if s.originalOut != nil {
out = s.originalOut
}
if w, _, err := terminalSize(out); err == nil {
return w
}
if isCygwinTerminal(out) {
tputCmd := exec.Command("tput", "cols")
tputCmd.Stdin = os.Stdin
if out, err := tputCmd.Output(); err == nil {
if w, err := strconv.Atoi(strings.TrimSpace(string(out))); err == nil {
return w
}
}
}
return defaultWidth
}
func (s *IOStreams) ColorScheme() *ColorScheme {
return NewColorScheme(s.ColorEnabled())
}
func System() *IOStreams {
stdoutIsTTY := isTerminal(os.Stdout)
stderrIsTTY := isTerminal(os.Stderr)
io := &IOStreams{
In: os.Stdin,
originalOut: os.Stdout,
Out: colorable.NewColorable(os.Stdout),
ErrOut: colorable.NewColorable(os.Stderr),
colorEnabled: os.Getenv("NO_COLOR") == "" && stdoutIsTTY,
}
if stdoutIsTTY && stderrIsTTY {
io.progressIndicatorEnabled = true
}
// prevent duplicate isTerminal queries now that we know the answer
io.SetStdoutTTY(stdoutIsTTY)
io.SetStderrTTY(stderrIsTTY)
return io
}
func Test() (*IOStreams, *bytes.Buffer, *bytes.Buffer, *bytes.Buffer) {
in := &bytes.Buffer{}
out := &bytes.Buffer{}
errOut := &bytes.Buffer{}
return &IOStreams{
In: ioutil.NopCloser(in),
Out: out,
ErrOut: errOut,
}, in, out, errOut
}
func isTerminal(f *os.File) bool {
return isatty.IsTerminal(f.Fd()) || isatty.IsCygwinTerminal(f.Fd())
}
func isCygwinTerminal(w io.Writer) bool {
if f, isFile := w.(*os.File); isFile {
return isatty.IsCygwinTerminal(f.Fd())
}
return false
}
func terminalSize(w io.Writer) (int, int, error) {
if f, isFile := w.(*os.File); isFile {
return terminal.GetSize(int(f.Fd()))
}
return 0, 0, fmt.Errorf("%v is not a file", w)
}