From 62549465a02194d2c309e870fd487d2da21fad89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Fri, 5 Jun 2020 17:38:36 +0200 Subject: [PATCH] Display JSON in indented, colored format in `api` output --- pkg/cmd/api/api.go | 17 +++++-- pkg/iostreams/iostreams.go | 27 +++++++++-- pkg/jsoncolor/jsoncolor.go | 95 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 133 insertions(+), 6 deletions(-) create mode 100644 pkg/jsoncolor/jsoncolor.go diff --git a/pkg/cmd/api/api.go b/pkg/cmd/api/api.go index 4158ff8c1..59db80332 100644 --- a/pkg/cmd/api/api.go +++ b/pkg/cmd/api/api.go @@ -6,11 +6,13 @@ import ( "io/ioutil" "net/http" "os" + "regexp" "strconv" "strings" "github.com/cli/cli/pkg/cmdutil" "github.com/cli/cli/pkg/iostreams" + "github.com/cli/cli/pkg/jsoncolor" "github.com/spf13/cobra" ) @@ -109,9 +111,18 @@ func apiRun(opts *ApiOptions) error { } defer resp.Body.Close() - _, err = io.Copy(opts.IO.Out, resp.Body) - if err != nil { - return err + isJSON, _ := regexp.MatchString(`[/+]json(;|$)`, resp.Header.Get("Content-Type")) + + if isJSON && opts.IO.ColorEnabled() { + err = jsoncolor.Write(opts.IO.Out, resp.Body, " ") + if err != nil { + return err + } + } else { + _, err = io.Copy(opts.IO.Out, resp.Body) + if err != nil { + return err + } } // TODO: detect GraphQL errors diff --git a/pkg/iostreams/iostreams.go b/pkg/iostreams/iostreams.go index 028b11264..8fd213e9f 100644 --- a/pkg/iostreams/iostreams.go +++ b/pkg/iostreams/iostreams.go @@ -5,19 +5,36 @@ import ( "io" "io/ioutil" "os" + + "github.com/mattn/go-colorable" + "github.com/mattn/go-isatty" ) type IOStreams struct { In io.ReadCloser Out io.Writer ErrOut io.Writer + + colorEnabled bool +} + +func (s *IOStreams) ColorEnabled() bool { + return s.colorEnabled } func System() *IOStreams { + var out io.Writer = os.Stdout + var colorEnabled bool + if os.Getenv("NO_COLOR") == "" && isTerminal(os.Stdout) { + out = colorable.NewColorable(os.Stdout) + colorEnabled = true + } + return &IOStreams{ - In: os.Stdin, - Out: os.Stdout, - ErrOut: os.Stderr, + In: os.Stdin, + Out: out, + ErrOut: os.Stderr, + colorEnabled: colorEnabled, } } @@ -31,3 +48,7 @@ func Test() (*IOStreams, *bytes.Buffer, *bytes.Buffer, *bytes.Buffer) { ErrOut: errOut, }, in, out, errOut } + +func isTerminal(f *os.File) bool { + return isatty.IsTerminal(f.Fd()) || isatty.IsCygwinTerminal(f.Fd()) +} diff --git a/pkg/jsoncolor/jsoncolor.go b/pkg/jsoncolor/jsoncolor.go new file mode 100644 index 000000000..3119c7d90 --- /dev/null +++ b/pkg/jsoncolor/jsoncolor.go @@ -0,0 +1,95 @@ +package jsoncolor + +import ( + "encoding/json" + "fmt" + "io" + "strings" +) + +const ( + colorDelim = "1;38" // bright white + colorKey = "1;34" // bright blue + colorNull = "1;30" // gray + colorString = "32" // green + colorBool = "33" // yellow +) + +func Write(w io.Writer, r io.Reader, indent string) error { + dec := json.NewDecoder(r) + dec.UseNumber() + + var idx int + var stack []json.Delim + + for { + t, err := dec.Token() + if err == io.EOF { + break + } + if err != nil { + return err + } + + switch tt := t.(type) { + case json.Delim: + switch tt { + case '{', '[': + stack = append(stack, tt) + idx = 0 + fmt.Fprintf(w, "\x1b[%sm%s\x1b[m", colorDelim, tt) + if dec.More() { + fmt.Fprint(w, "\n", strings.Repeat(indent, len(stack))) + } + continue + case '}', ']': + stack = stack[:len(stack)-1] + idx = 0 + fmt.Fprintf(w, "\x1b[%sm%s\x1b[m", colorDelim, tt) + } + default: + b, err := json.Marshal(tt) + if err != nil { + return err + } + + isKey := len(stack) > 0 && stack[len(stack)-1] == '{' && idx%2 == 0 + idx++ + + var color string + if isKey { + color = colorKey + } else if tt == nil { + color = colorNull + } else { + switch t.(type) { + case string: + color = colorString + case bool: + color = colorBool + } + } + + if color == "" { + _, _ = w.Write(b) + } else { + fmt.Fprintf(w, "\x1b[%sm%s\x1b[m", color, b) + } + + if isKey { + fmt.Fprintf(w, "\x1b[%sm:\x1b[m ", colorDelim) + continue + } + } + + if dec.More() { + fmt.Fprintf(w, "\x1b[%sm,\x1b[m\n%s", colorDelim, strings.Repeat(indent, len(stack))) + } else if len(stack) > 0 { + fmt.Fprint(w, "\n", strings.Repeat(indent, len(stack)-1)) + } else { + fmt.Fprint(w, "\n") + } + } + + return nil +}