From c1dc9fcdf2efaffb8222b7159716b58e0961239a Mon Sep 17 00:00:00 2001 From: abdfnx Date: Fri, 4 Feb 2022 14:38:25 +0300 Subject: [PATCH] finish from `ios` package, add new go modules --- go.mod | 6 +- go.sum | 3 + ios/iostreams.go | 434 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 441 insertions(+), 2 deletions(-) create mode 100755 ios/iostreams.go diff --git a/go.mod b/go.mod index da76158..2f47355 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.17 require ( github.com/AlecAivazis/survey/v2 v2.3.2 + github.com/abdfnx/looker v0.1.0 github.com/abdfnx/resto v0.1.6 github.com/alecthomas/chroma v0.10.0 github.com/briandowns/spinner v1.18.0 @@ -12,10 +13,13 @@ require ( github.com/charmbracelet/glamour v0.5.0 github.com/charmbracelet/lipgloss v0.4.0 github.com/disintegration/imaging v1.6.2 + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/gorilla/websocket v1.4.2 github.com/klauspost/pgzip v1.2.5 github.com/ledongthuc/pdf v0.0.0-20210621053716-e28cb8259002 github.com/lucasb-eyer/go-colorful v1.2.0 + github.com/mattn/go-colorable v0.1.12 + github.com/mattn/go-isatty v0.0.14 github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d github.com/muesli/reflow v0.3.0 github.com/muesli/termenv v0.9.0 @@ -42,8 +46,6 @@ require ( github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/klauspost/compress v1.14.2 // indirect github.com/magiconair/properties v1.8.5 // indirect - github.com/mattn/go-colorable v0.1.12 // indirect - github.com/mattn/go-isatty v0.0.14 // indirect github.com/mattn/go-runewidth v0.0.13 // indirect github.com/microcosm-cc/bluemonday v1.0.17 // indirect github.com/mitchellh/mapstructure v1.4.3 // indirect diff --git a/go.sum b/go.sum index 4d316a8..0ea7b32 100644 --- a/go.sum +++ b/go.sum @@ -55,6 +55,8 @@ github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6 github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/Timothee-Cardoso/tc-exe v1.0.1/go.mod h1:dq8JjfgZPLtVNCSxRckj9GPhjaCm7IifIyqmkUgwAiM= +github.com/abdfnx/looker v0.1.0 h1:tMN7E0wKIgbydAPPQ1RkppJ1bGHn+B+y9PZy7mwa+3U= +github.com/abdfnx/looker v0.1.0/go.mod h1:QVfPHnredPBUg4R+MtEkZbMBbqrgtoaj0JHO3KYkvyE= github.com/abdfnx/resto v0.1.6 h1:yOM9O9bpMP4lb2ox0U7/gcFXO78P5eUZBxWuKrfdrFA= github.com/abdfnx/resto v0.1.6/go.mod h1:7+/dYHN1Zw70GKAOtT+76LG9ZnMqA9NTbUFFgEt7rsk= github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek= @@ -220,6 +222,7 @@ github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= diff --git a/ios/iostreams.go b/ios/iostreams.go new file mode 100755 index 0000000..3edf50b --- /dev/null +++ b/ios/iostreams.go @@ -0,0 +1,434 @@ +package ios + +import ( + "io" + "os" + "fmt" + "time" + "bytes" + "errors" + "os/exec" + "strconv" + "strings" + "io/ioutil" + + "golang.org/x/term" + "github.com/google/shlex" + "github.com/abdfnx/looker" + "github.com/muesli/termenv" + "github.com/mattn/go-isatty" + "github.com/mattn/go-colorable" + "github.com/briandowns/spinner" +) + +const DefaultWidth = 80 + +type IOStreams struct { + In io.ReadCloser + Out io.Writer + ErrOut io.Writer + + // the original (non-colorable) output stream + originalOut io.Writer + colorEnabled bool + is256enabled bool + hasTrueColor bool + terminalTheme string + + progressIndicatorEnabled bool + progressIndicator *spinner.Spinner + + stdinTTYOverride bool + stdinIsTTY bool + stdoutTTYOverride bool + stdoutIsTTY bool + stderrTTYOverride bool + stderrIsTTY bool + termWidthOverride int + ttySize func() (int, int, error) + + pagerCommand string + pagerProcess *os.Process + + neverPrompt bool + + TempFileOverride *os.File +} + +func (s *IOStreams) ColorEnabled() bool { + return s.colorEnabled +} + +func (s *IOStreams) ColorSupport256() bool { + return s.is256enabled +} + +func (s *IOStreams) HasTrueColor() bool { + return s.hasTrueColor +} + +func (s *IOStreams) DetectTerminalTheme() string { + if !s.ColorEnabled() { + s.terminalTheme = "none" + return "none" + } + + if s.pagerProcess != nil { + s.terminalTheme = "none" + return "none" + } + + style := os.Getenv("GLAMOUR_STYLE") + if style != "" && style != "auto" { + s.terminalTheme = "none" + return "none" + } + + if termenv.HasDarkBackground() { + s.terminalTheme = "dark" + return "dark" + } + + s.terminalTheme = "light" + return "light" +} + +func (s *IOStreams) TerminalTheme() string { + if s.terminalTheme == "" { + return "none" + } + + return s.terminalTheme +} + +func (s *IOStreams) SetColorEnabled(colorEnabled bool) { + s.colorEnabled = 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) SetPager(cmd string) { + s.pagerCommand = cmd +} + +func (s *IOStreams) GetPager() string { + return s.pagerCommand +} + +func (s *IOStreams) StartPager() error { + if s.pagerCommand == "" || s.pagerCommand == "cat" || !s.IsStdoutTTY() { + return nil + } + + pagerArgs, err := shlex.Split(s.pagerCommand) + + if err != nil { + return err + } + + pagerEnv := os.Environ() + + for i := len(pagerEnv) - 1; i >= 0; i-- { + if strings.HasPrefix(pagerEnv[i], "PAGER=") { + pagerEnv = append(pagerEnv[0:i], pagerEnv[i+1:]...) + } + } + + if _, ok := os.LookupEnv("LESS"); !ok { + pagerEnv = append(pagerEnv, "LESS=FRX") + } + + if _, ok := os.LookupEnv("LV"); !ok { + pagerEnv = append(pagerEnv, "LV=-c") + } + + pagerExe, err := looker.LookPath(pagerArgs[0]) + + if err != nil { + return err + } + + pagerCmd := exec.Command(pagerExe, pagerArgs[1:]...) + pagerCmd.Env = pagerEnv + pagerCmd.Stdout = s.Out + pagerCmd.Stderr = s.ErrOut + pagedOut, err := pagerCmd.StdinPipe() + + if err != nil { + return err + } + + s.Out = pagedOut + err = pagerCmd.Start() + if err != nil { + return err + } + + s.pagerProcess = pagerCmd.Process + + return nil +} + +func (s *IOStreams) StopPager() { + if s.pagerProcess == nil { + return + } + + _ = s.Out.(io.ReadCloser).Close() + _, _ = s.pagerProcess.Wait() + s.pagerProcess = nil +} + +func (s *IOStreams) CanPrompt() bool { + if s.neverPrompt { + return false + } + + return s.IsStdinTTY() && s.IsStdoutTTY() +} + +func (s *IOStreams) GetNeverPrompt() bool { + return s.neverPrompt +} + +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 +} + +// TerminalWidth returns the width of the terminal that stdout is attached to. +// TODO: investigate whether ProcessTerminalWidth could replace all this. +func (s *IOStreams) TerminalWidth() int { + if s.termWidthOverride > 0 { + return s.termWidthOverride + } + + defaultWidth := DefaultWidth + out := s.Out + + if s.originalOut != nil { + out = s.originalOut + } + + if w, _, err := terminalSize(out); err == nil { + return w + } + + if isCygwinTerminal(out) { + tputExe, err := looker.LookPath("tput") + if err != nil { + return defaultWidth + } + + tputCmd := exec.Command(tputExe, "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 +} + +// ProcessTerminalWidth returns the width of the terminal that the process is attached to. +func (s *IOStreams) ProcessTerminalWidth() int { + w, _, err := s.ttySize() + if err != nil { + return DefaultWidth + } + + return w +} + +func (s *IOStreams) ForceTerminal(spec string) { + s.colorEnabled = !EnvColorDisabled() + s.SetStdoutTTY(true) + + if w, err := strconv.Atoi(spec); err == nil { + s.termWidthOverride = w + return + } + + ttyWidth, _, err := s.ttySize() + + if err != nil { + return + } + + s.termWidthOverride = ttyWidth + + if strings.HasSuffix(spec, "%") { + if p, err := strconv.Atoi(spec[:len(spec)-1]); err == nil { + s.termWidthOverride = int(float64(s.termWidthOverride) * (float64(p) / 100)) + } + } +} + +func (s *IOStreams) ColorScheme() *ColorScheme { + return NewColorScheme(s.ColorEnabled(), s.ColorSupport256()) +} + +func (s *IOStreams) ReadUserFile(fn string) ([]byte, error) { + var r io.ReadCloser + if fn == "-" { + r = s.In + } else { + var err error + r, err = os.Open(fn) + if err != nil { + return nil, err + } + } + + defer r.Close() + + return ioutil.ReadAll(r) +} + +func (s *IOStreams) TempFile(dir, pattern string) (*os.File, error) { + if s.TempFileOverride != nil { + return s.TempFileOverride, nil + } + + return ioutil.TempFile(dir, pattern) +} + +func System() *IOStreams { + stdoutIsTTY := isTerminal(os.Stdout) + stderrIsTTY := isTerminal(os.Stderr) + + assumeTrueColor := false + + if stdoutIsTTY { + if err := enableVirtualTerminalProcessing(os.Stdout); err == nil { + assumeTrueColor = true + } + } + + io := &IOStreams{ + In: os.Stdin, + originalOut: os.Stdout, + Out: colorable.NewColorable(os.Stdout), + ErrOut: colorable.NewColorable(os.Stderr), + colorEnabled: EnvColorForced() || (!EnvColorDisabled() && stdoutIsTTY), + is256enabled: assumeTrueColor || Is256ColorSupported(), + hasTrueColor: assumeTrueColor || IsTrueColorSupported(), + ttySize: ttySize, + } + + 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, + ttySize: func() (int, int, error) { + return -1, -1, errors.New("ttySize not implemented in tests") + }, + }, 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 +} + +// terminalSize measures the viewport of the terminal that the output stream is connected to +func terminalSize(w io.Writer) (int, int, error) { + if f, isFile := w.(*os.File); isFile { + return term.GetSize(int(f.Fd())) + } + + return 0, 0, fmt.Errorf("%v is not a file", w) +}