diff --git a/go.mod b/go.mod index f1745cb..bb2a994 100644 --- a/go.mod +++ b/go.mod @@ -7,12 +7,14 @@ require ( github.com/BurntSushi/toml v0.3.1 github.com/MakeNowJust/heredoc/v2 v2.0.1 github.com/daixiang0/gci v0.2.8 + github.com/fatih/color v1.9.0 github.com/fujiwara/shapeio v1.0.0 github.com/gocarina/gocsv v0.0.0-20210408192840-02d7211d929d github.com/golang/mock v1.5.0 github.com/google/go-cmp v0.5.5 github.com/google/go-jsonnet v0.17.0 github.com/mattn/go-isatty v0.0.13 + github.com/olekukonko/tablewriter v0.0.5 github.com/stretchr/testify v1.7.0 github.com/urfave/cli/v2 v2.3.0 golang.org/x/text v0.3.3 diff --git a/go.sum b/go.sum index c2bfd57..3559abb 100644 --- a/go.sum +++ b/go.sum @@ -15,6 +15,7 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fujiwara/shapeio v1.0.0 h1:xG5D9oNqCSUUbryZ/jQV3cqe1v2suEjwPIcEg1gKM8M= github.com/fujiwara/shapeio v1.0.0/go.mod h1:LmEmu6L/8jetyj1oewewFb7bZCNRwE7wLCUNzDLaLVA= @@ -45,8 +46,12 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA= github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= diff --git a/pkg/template/color.go b/pkg/template/color.go new file mode 100644 index 0000000..342b2b9 --- /dev/null +++ b/pkg/template/color.go @@ -0,0 +1,18 @@ +package template + +import ( + "github.com/fatih/color" + "github.com/olekukonko/tablewriter" +) + +var ( + colorTitle = color.New(color.FgYellow, color.Bold) + colorTotalAdd = color.New(color.FgHiGreen, color.Bold) + colorTotalRemove = color.New(color.FgRed, color.Bold) + + noColor = tablewriter.Colors{} + colorAdd = tablewriter.Colors{tablewriter.FgHiGreenColor, tablewriter.Bold} + colorFooter = tablewriter.Color(tablewriter.FgHiBlueColor, tablewriter.Bold) + colorHeader = tablewriter.Colors{tablewriter.FgHiCyanColor, tablewriter.Bold} + colorRemove = tablewriter.Colors{tablewriter.FgRedColor, tablewriter.Bold} +) diff --git a/pkg/template/diff_printer.go b/pkg/template/diff_printer.go new file mode 100644 index 0000000..cf7dc6c --- /dev/null +++ b/pkg/template/diff_printer.go @@ -0,0 +1,180 @@ +package template + +import ( + "fmt" + "io" + "strconv" + "strings" + + "github.com/olekukonko/tablewriter" +) + +type DiffPrinter struct { + w io.Writer + writer *tablewriter.Table + + useColor bool + title string + + appendCalls int + headerLen int +} + +func NewDiffPrinter(w io.Writer, hasColor, hasBorder bool) *DiffPrinter { + wr := tablewriter.NewWriter(w) + wr.SetBorder(hasBorder) + wr.SetRowLine(hasBorder) + + return &DiffPrinter{ + w: w, + writer: wr, + useColor: hasColor, + } +} + +func (d *DiffPrinter) Render() { + if d.appendCalls == 0 { + return + } + + // set the title and the add/remove legend + title := strings.ToUpper(d.title) + add := "+add" + remove := "-remove" + if d.useColor { + title = colorTitle.Sprint(title) + add = colorTotalAdd.Sprint(add) + remove = colorTotalRemove.Sprint(remove) + } + fmt.Fprintf(d.w, "%s %s | %s | unchanged\n", title, add, remove) + + d.setFooter() + d.writer.Render() +} + +func (d *DiffPrinter) Title(title string) *DiffPrinter { + d.title = title + return d +} + +func (d *DiffPrinter) SetHeaders(headers ...string) *DiffPrinter { + headers = d.prepend(headers, "+/-") + d.headerLen = len(headers) + + d.writer.SetHeader(headers) + + headerColors := make([]tablewriter.Colors, d.headerLen) + color := noColor + if d.useColor { + color = colorHeader + } + for i := range headerColors { + headerColors[i] = color + } + d.writer.SetHeaderColor(headerColors...) + + return d +} + +func (d *DiffPrinter) setFooter() *DiffPrinter { + footers := make([]string, d.headerLen) + if d.headerLen > 1 { + footers[len(footers)-2] = "TOTAL" + footers[len(footers)-1] = strconv.Itoa(d.appendCalls) + } else { + footers[0] = "TOTAL: " + strconv.Itoa(d.appendCalls) + } + + d.writer.SetFooter(footers) + colors := make([]tablewriter.Colors, d.headerLen) + color := noColor + if d.useColor { + color = colorFooter + } + if d.headerLen > 1 { + colors[len(colors)-2] = color + colors[len(colors)-1] = color + } else { + colors[0] = color + } + d.writer.SetFooterColor(colors...) + + return d +} + +func (d *DiffPrinter) Append(slc []string) { + d.writer.Append(d.prepend(slc, "")) +} + +func (d *DiffPrinter) AppendDiff(remove, add []string) { + defer func() { d.appendCalls++ }() + + if d.appendCalls > 0 { + d.appendBufferLine() + } + + lenAdd, lenRemove := len(add), len(remove) + preppedAdd, preppedRemove := d.prepend(add, "+"), d.prepend(remove, "-") + if lenRemove > 0 && lenAdd == 0 { + d.writer.Rich(preppedRemove, d.redRow(len(preppedRemove))) + return + } + if lenAdd > 0 && lenRemove == 0 { + d.writer.Rich(preppedAdd, d.greenRow(len(preppedAdd))) + return + } + + var ( + addColors = make([]tablewriter.Colors, len(preppedAdd)) + removeColors = make([]tablewriter.Colors, len(preppedRemove)) + hasDiff bool + ) + addColor, removeColor := noColor, noColor + if d.useColor { + addColor, removeColor = colorAdd, colorRemove + } + for i := 0; i < lenRemove; i++ { + if add[i] != remove[i] { + hasDiff = true + // offset to skip prepended +/- column + addColors[i+1], removeColors[i+1] = addColor, removeColor + } + } + + if !hasDiff { + d.writer.Append(d.prepend(add, "")) + return + } + + addColors[0], removeColors[0] = addColor, removeColor + d.writer.Rich(d.prepend(remove, "-"), removeColors) + d.writer.Rich(d.prepend(add, "+"), addColors) +} + +func (d *DiffPrinter) appendBufferLine() { + d.writer.Append([]string{}) +} + +func (d *DiffPrinter) redRow(i int) []tablewriter.Colors { + return d.colorRow(colorRemove, i) +} + +func (d *DiffPrinter) greenRow(i int) []tablewriter.Colors { + return d.colorRow(colorAdd, i) +} + +func (d *DiffPrinter) prepend(slc []string, val string) []string { + return append([]string{val}, slc...) +} + +func (d *DiffPrinter) colorRow(color tablewriter.Colors, i int) []tablewriter.Colors { + colors := make([]tablewriter.Colors, i) + for i := range colors { + if d.useColor { + colors[i] = color + } else { + colors[i] = noColor + } + } + return colors +} diff --git a/pkg/template/diff_printer_test.go b/pkg/template/diff_printer_test.go new file mode 100644 index 0000000..6fa11a9 --- /dev/null +++ b/pkg/template/diff_printer_test.go @@ -0,0 +1,67 @@ +package template_test + +import ( + "bytes" + "testing" + + "github.com/influxdata/influx-cli/v2/pkg/template" + "github.com/stretchr/testify/require" +) + +func TestDiffPrinter_Empty(t *testing.T) { + t.Parallel() + + out := bytes.Buffer{} + printer := template.NewDiffPrinter(&out, false, true). + Title("Example"). + SetHeaders("Wow", "Such", "A", "Fancy", "Printer") + + printer.Render() + require.Empty(t, out.String()) +} + +func TestDiffPrinter(t *testing.T) { + t.Parallel() + + out := bytes.Buffer{} + printer := template.NewDiffPrinter(&out, false, true). + Title("Example"). + SetHeaders("Wow", "Such", "A", "Fancy", "Printer") + + // Add + printer.AppendDiff(nil, []string{"A", "B", "C", "D", "E"}) + + // No change + printer.Append([]string{"foo", "bar", "baz", "qux", "wat"}) + + // Replace + printer.AppendDiff( + []string{"1", "200000000000000", "3", "4", "5"}, + []string{"9", "8", "7", "6", "5"}, + ) + + // Remove + printer.AppendDiff([]string{"x y", "z x", "x y z", "", "y z"}, nil) + + printer.Render() + expected := `EXAMPLE +add | -remove | unchanged ++-----+-----+-----------------+-------+-------+---------+ +| +/- | WOW | SUCH | A | FANCY | PRINTER | ++-----+-----+-----------------+-------+-------+---------+ +| + | A | B | C | D | E | ++-----+-----+-----------------+-------+-------+---------+ +| | foo | bar | baz | qux | wat | ++-----+-----+-----------------+-------+-------+---------+ ++-----+-----+-----------------+-------+-------+---------+ +| - | 1 | 200000000000000 | 3 | 4 | 5 | ++-----+-----+-----------------+-------+-------+---------+ +| + | 9 | 8 | 7 | 6 | 5 | ++-----+-----+-----------------+-------+-------+---------+ ++-----+-----+-----------------+-------+-------+---------+ +| - | x y | z x | x y z | | y z | ++-----+-----+-----------------+-------+-------+---------+ +| TOTAL | 3 | ++-----+-----+-----------------+-------+-------+---------+ +` + require.Equal(t, expected, out.String()) +} diff --git a/pkg/template/table_printer.go b/pkg/template/table_printer.go new file mode 100644 index 0000000..d650df5 --- /dev/null +++ b/pkg/template/table_printer.go @@ -0,0 +1,110 @@ +package template + +import ( + "fmt" + "io" + "strconv" + "strings" + + "github.com/olekukonko/tablewriter" +) + +type TablePrinter struct { + w io.Writer + writer *tablewriter.Table + + useColor bool + title string + + headerLen int + appendCalls int +} + +func NewTablePrinter(w io.Writer, hasColor, hasBorder bool) *TablePrinter { + wr := tablewriter.NewWriter(w) + wr.SetBorder(hasBorder) + wr.SetRowLine(hasBorder) + + return &TablePrinter{ + w: w, + writer: wr, + useColor: hasColor, + } +} + +func (t *TablePrinter) Render() { + if t.appendCalls == 0 { + return + } + + title := strings.ToUpper(t.title) + if t.useColor { + title = colorTitle.Sprint(title) + } + fmt.Fprintln(t.w, title) + + t.setFooter() + t.writer.Render() +} + +func (t *TablePrinter) Title(title string) *TablePrinter { + t.title = title + return t +} + +func (t *TablePrinter) SetHeaders(headers ...string) *TablePrinter { + t.headerLen = len(headers) + t.writer.SetHeader(headers) + + headerColors := make([]tablewriter.Colors, t.headerLen) + alignments := make([]int, t.headerLen) + + color := noColor + if t.useColor { + color = colorHeader + } + for i, header := range headers { + headerColors[i] = color + if strings.EqualFold("description", header) { + t.writer.SetColMinWidth(i, 30) + alignments[i] = tablewriter.ALIGN_LEFT + } else { + alignments[i] = tablewriter.ALIGN_CENTER + } + } + t.writer.SetHeaderColor(headerColors...) + t.writer.SetColumnAlignment(alignments) + + return t +} + +func (t *TablePrinter) setFooter() *TablePrinter { + footers := make([]string, t.headerLen) + if t.headerLen > 1 { + footers[len(footers)-2] = "TOTAL" + footers[len(footers)-1] = strconv.Itoa(t.appendCalls) + } else { + footers[0] = "TOTAL: " + strconv.Itoa(t.appendCalls) + } + t.writer.SetFooter(footers) + + colors := make([]tablewriter.Colors, t.headerLen) + color := noColor + if t.useColor { + color = colorFooter + } + if t.headerLen > 1 { + colors[len(colors)-2] = color + colors[len(colors)-1] = color + } else { + colors[0] = color + } + t.writer.SetFooterColor(colors...) + + return t +} + +func (t *TablePrinter) Append(slc []string) { + t.appendCalls++ + t.writer.Append(slc) +} diff --git a/pkg/template/table_printer_test.go b/pkg/template/table_printer_test.go new file mode 100644 index 0000000..9793ed0 --- /dev/null +++ b/pkg/template/table_printer_test.go @@ -0,0 +1,70 @@ +package template_test + +import ( + "bytes" + "testing" + + "github.com/influxdata/influx-cli/v2/pkg/template" + "github.com/stretchr/testify/require" +) + +func TestTablePrinter_Empty(t *testing.T) { + t.Parallel() + + out := bytes.Buffer{} + printer := template.NewTablePrinter(&out, false, true). + Title("Example"). + SetHeaders("Wow", "Such", "A", "Fancy", "Printer") + + printer.Render() + require.Empty(t, out.String()) +} + +func TestTablePrinter(t *testing.T) { + t.Parallel() + + out := bytes.Buffer{} + printer := template.NewTablePrinter(&out, false, true). + Title("Example"). + SetHeaders("Wow", "Such", "A", "Fancy", "Printer") + + printer.Append([]string{"foo", "bar", "baz", "qux", "wat"}) + printer.Append([]string{"veryveryverylongggg", "", "a", "b", "c"}) + + printer.Render() + expected := `EXAMPLE ++---------------------+------+-----+-------+---------+ +| WOW | SUCH | A | FANCY | PRINTER | ++---------------------+------+-----+-------+---------+ +| foo | bar | baz | qux | wat | ++---------------------+------+-----+-------+---------+ +| veryveryverylongggg | | a | b | c | ++---------------------+------+-----+-------+---------+ +| TOTAL | 2 | ++---------------------+------+-----+-------+---------+ +` + require.Equal(t, expected, out.String()) +} + +func TestTablePrinter_Description(t *testing.T) { + t.Parallel() + + out := bytes.Buffer{} + printer := template.NewTablePrinter(&out, false, true). + Title("Example"). + SetHeaders("Wow", "Such", "A", "Fancy", "Description") + printer.Append([]string{"once", "upon", "a", "time", "short description"}) + + printer.Render() + // Expect that the description is left-aligned with a min width. + expected := `EXAMPLE ++------+------+---+-------+--------------------------------+ +| WOW | SUCH | A | FANCY | DESCRIPTION | ++------+------+---+-------+--------------------------------+ +| once | upon | a | time | short description | ++------+------+---+-------+--------------------------------+ +| TOTAL | 1 | ++------+------+---+-------+--------------------------------+ +` + require.Equal(t, expected, out.String()) +}