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 }