github.com/abdfnx/gh-api@v0.0.0-20210414084727-f5432eec23b8/pkg/iostreams/iostreams.go (about)

     1  package iostreams
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"io/ioutil"
     8  	"os"
     9  	"os/exec"
    10  	"strconv"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/briandowns/spinner"
    15  	"github.com/cli/safeexec"
    16  	"github.com/google/shlex"
    17  	"github.com/mattn/go-colorable"
    18  	"github.com/mattn/go-isatty"
    19  	"github.com/muesli/termenv"
    20  	"golang.org/x/crypto/ssh/terminal"
    21  )
    22  
    23  type IOStreams struct {
    24  	In     io.ReadCloser
    25  	Out    io.Writer
    26  	ErrOut io.Writer
    27  
    28  	// the original (non-colorable) output stream
    29  	originalOut   io.Writer
    30  	colorEnabled  bool
    31  	is256enabled  bool
    32  	terminalTheme string
    33  
    34  	progressIndicatorEnabled bool
    35  	progressIndicator        *spinner.Spinner
    36  
    37  	stdinTTYOverride  bool
    38  	stdinIsTTY        bool
    39  	stdoutTTYOverride bool
    40  	stdoutIsTTY       bool
    41  	stderrTTYOverride bool
    42  	stderrIsTTY       bool
    43  
    44  	pagerCommand string
    45  	pagerProcess *os.Process
    46  
    47  	neverPrompt bool
    48  
    49  	TempFileOverride *os.File
    50  }
    51  
    52  func (s *IOStreams) ColorEnabled() bool {
    53  	return s.colorEnabled
    54  }
    55  
    56  func (s *IOStreams) ColorSupport256() bool {
    57  	return s.is256enabled
    58  }
    59  
    60  func (s *IOStreams) DetectTerminalTheme() string {
    61  	if !s.ColorEnabled() {
    62  		s.terminalTheme = "none"
    63  		return "none"
    64  	}
    65  
    66  	if s.pagerProcess != nil {
    67  		s.terminalTheme = "none"
    68  		return "none"
    69  	}
    70  
    71  	style := os.Getenv("GLAMOUR_STYLE")
    72  	if style != "" && style != "auto" {
    73  		s.terminalTheme = "none"
    74  		return "none"
    75  	}
    76  
    77  	if termenv.HasDarkBackground() {
    78  		s.terminalTheme = "dark"
    79  		return "dark"
    80  	}
    81  
    82  	s.terminalTheme = "light"
    83  	return "light"
    84  }
    85  
    86  func (s *IOStreams) TerminalTheme() string {
    87  	if s.terminalTheme == "" {
    88  		return "none"
    89  	}
    90  
    91  	return s.terminalTheme
    92  }
    93  
    94  func (s *IOStreams) SetStdinTTY(isTTY bool) {
    95  	s.stdinTTYOverride = true
    96  	s.stdinIsTTY = isTTY
    97  }
    98  
    99  func (s *IOStreams) IsStdinTTY() bool {
   100  	if s.stdinTTYOverride {
   101  		return s.stdinIsTTY
   102  	}
   103  	if stdin, ok := s.In.(*os.File); ok {
   104  		return isTerminal(stdin)
   105  	}
   106  	return false
   107  }
   108  
   109  func (s *IOStreams) SetStdoutTTY(isTTY bool) {
   110  	s.stdoutTTYOverride = true
   111  	s.stdoutIsTTY = isTTY
   112  }
   113  
   114  func (s *IOStreams) IsStdoutTTY() bool {
   115  	if s.stdoutTTYOverride {
   116  		return s.stdoutIsTTY
   117  	}
   118  	if stdout, ok := s.Out.(*os.File); ok {
   119  		return isTerminal(stdout)
   120  	}
   121  	return false
   122  }
   123  
   124  func (s *IOStreams) SetStderrTTY(isTTY bool) {
   125  	s.stderrTTYOverride = true
   126  	s.stderrIsTTY = isTTY
   127  }
   128  
   129  func (s *IOStreams) IsStderrTTY() bool {
   130  	if s.stderrTTYOverride {
   131  		return s.stderrIsTTY
   132  	}
   133  	if stderr, ok := s.ErrOut.(*os.File); ok {
   134  		return isTerminal(stderr)
   135  	}
   136  	return false
   137  }
   138  
   139  func (s *IOStreams) SetPager(cmd string) {
   140  	s.pagerCommand = cmd
   141  }
   142  
   143  func (s *IOStreams) StartPager() error {
   144  	if s.pagerCommand == "" || s.pagerCommand == "cat" || !s.IsStdoutTTY() {
   145  		return nil
   146  	}
   147  
   148  	pagerArgs, err := shlex.Split(s.pagerCommand)
   149  	if err != nil {
   150  		return err
   151  	}
   152  
   153  	pagerEnv := os.Environ()
   154  	for i := len(pagerEnv) - 1; i >= 0; i-- {
   155  		if strings.HasPrefix(pagerEnv[i], "PAGER=") {
   156  			pagerEnv = append(pagerEnv[0:i], pagerEnv[i+1:]...)
   157  		}
   158  	}
   159  	if _, ok := os.LookupEnv("LESS"); !ok {
   160  		pagerEnv = append(pagerEnv, "LESS=FRX")
   161  	}
   162  	if _, ok := os.LookupEnv("LV"); !ok {
   163  		pagerEnv = append(pagerEnv, "LV=-c")
   164  	}
   165  
   166  	pagerExe, err := safeexec.LookPath(pagerArgs[0])
   167  	if err != nil {
   168  		return err
   169  	}
   170  	pagerCmd := exec.Command(pagerExe, pagerArgs[1:]...)
   171  	pagerCmd.Env = pagerEnv
   172  	pagerCmd.Stdout = s.Out
   173  	pagerCmd.Stderr = s.ErrOut
   174  	pagedOut, err := pagerCmd.StdinPipe()
   175  	if err != nil {
   176  		return err
   177  	}
   178  	s.Out = pagedOut
   179  	err = pagerCmd.Start()
   180  	if err != nil {
   181  		return err
   182  	}
   183  	s.pagerProcess = pagerCmd.Process
   184  	return nil
   185  }
   186  
   187  func (s *IOStreams) StopPager() {
   188  	if s.pagerProcess == nil {
   189  		return
   190  	}
   191  
   192  	_ = s.Out.(io.ReadCloser).Close()
   193  	_, _ = s.pagerProcess.Wait()
   194  	s.pagerProcess = nil
   195  }
   196  
   197  func (s *IOStreams) CanPrompt() bool {
   198  	if s.neverPrompt {
   199  		return false
   200  	}
   201  
   202  	return s.IsStdinTTY() && s.IsStdoutTTY()
   203  }
   204  
   205  func (s *IOStreams) SetNeverPrompt(v bool) {
   206  	s.neverPrompt = v
   207  }
   208  
   209  func (s *IOStreams) StartProgressIndicator() {
   210  	if !s.progressIndicatorEnabled {
   211  		return
   212  	}
   213  	sp := spinner.New(spinner.CharSets[11], 400*time.Millisecond, spinner.WithWriter(s.ErrOut))
   214  	sp.Start()
   215  	s.progressIndicator = sp
   216  }
   217  
   218  func (s *IOStreams) StopProgressIndicator() {
   219  	if s.progressIndicator == nil {
   220  		return
   221  	}
   222  	s.progressIndicator.Stop()
   223  	s.progressIndicator = nil
   224  }
   225  
   226  func (s *IOStreams) TerminalWidth() int {
   227  	defaultWidth := 80
   228  	out := s.Out
   229  	if s.originalOut != nil {
   230  		out = s.originalOut
   231  	}
   232  
   233  	if w, _, err := terminalSize(out); err == nil {
   234  		return w
   235  	}
   236  
   237  	if isCygwinTerminal(out) {
   238  		tputExe, err := safeexec.LookPath("tput")
   239  		if err != nil {
   240  			return defaultWidth
   241  		}
   242  		tputCmd := exec.Command(tputExe, "cols")
   243  		tputCmd.Stdin = os.Stdin
   244  		if out, err := tputCmd.Output(); err == nil {
   245  			if w, err := strconv.Atoi(strings.TrimSpace(string(out))); err == nil {
   246  				return w
   247  			}
   248  		}
   249  	}
   250  
   251  	return defaultWidth
   252  }
   253  
   254  func (s *IOStreams) ColorScheme() *ColorScheme {
   255  	return NewColorScheme(s.ColorEnabled(), s.ColorSupport256())
   256  }
   257  
   258  func (s *IOStreams) ReadUserFile(fn string) ([]byte, error) {
   259  	var r io.ReadCloser
   260  	if fn == "-" {
   261  		r = s.In
   262  	} else {
   263  		var err error
   264  		r, err = os.Open(fn)
   265  		if err != nil {
   266  			return nil, err
   267  		}
   268  	}
   269  	defer r.Close()
   270  	return ioutil.ReadAll(r)
   271  }
   272  
   273  func (s *IOStreams) TempFile(dir, pattern string) (*os.File, error) {
   274  	if s.TempFileOverride != nil {
   275  		return s.TempFileOverride, nil
   276  	}
   277  	return ioutil.TempFile(dir, pattern)
   278  }
   279  
   280  func System() *IOStreams {
   281  	stdoutIsTTY := isTerminal(os.Stdout)
   282  	stderrIsTTY := isTerminal(os.Stderr)
   283  
   284  	var pagerCommand string
   285  	if ghPager, ghPagerExists := os.LookupEnv("GH_PAGER"); ghPagerExists {
   286  		pagerCommand = ghPager
   287  	} else {
   288  		pagerCommand = os.Getenv("PAGER")
   289  	}
   290  
   291  	io := &IOStreams{
   292  		In:           os.Stdin,
   293  		originalOut:  os.Stdout,
   294  		Out:          colorable.NewColorable(os.Stdout),
   295  		ErrOut:       colorable.NewColorable(os.Stderr),
   296  		colorEnabled: EnvColorForced() || (!EnvColorDisabled() && stdoutIsTTY),
   297  		is256enabled: Is256ColorSupported(),
   298  		pagerCommand: pagerCommand,
   299  	}
   300  
   301  	if stdoutIsTTY && stderrIsTTY {
   302  		io.progressIndicatorEnabled = true
   303  	}
   304  
   305  	// prevent duplicate isTerminal queries now that we know the answer
   306  	io.SetStdoutTTY(stdoutIsTTY)
   307  	io.SetStderrTTY(stderrIsTTY)
   308  	return io
   309  }
   310  
   311  func Test() (*IOStreams, *bytes.Buffer, *bytes.Buffer, *bytes.Buffer) {
   312  	in := &bytes.Buffer{}
   313  	out := &bytes.Buffer{}
   314  	errOut := &bytes.Buffer{}
   315  	return &IOStreams{
   316  		In:     ioutil.NopCloser(in),
   317  		Out:    out,
   318  		ErrOut: errOut,
   319  	}, in, out, errOut
   320  }
   321  
   322  func isTerminal(f *os.File) bool {
   323  	return isatty.IsTerminal(f.Fd()) || isatty.IsCygwinTerminal(f.Fd())
   324  }
   325  
   326  func isCygwinTerminal(w io.Writer) bool {
   327  	if f, isFile := w.(*os.File); isFile {
   328  		return isatty.IsCygwinTerminal(f.Fd())
   329  	}
   330  	return false
   331  }
   332  
   333  func terminalSize(w io.Writer) (int, int, error) {
   334  	if f, isFile := w.(*os.File); isFile {
   335  		return terminal.GetSize(int(f.Fd()))
   336  	}
   337  	return 0, 0, fmt.Errorf("%v is not a file", w)
   338  }