From 0fa582537f3282699f2e64b9632eb68c7d885bbf Mon Sep 17 00:00:00 2001 From: Aerex Date: Mon, 11 Nov 2024 14:49:31 -0600 Subject: [PATCH] feat: used go-pretty to improved table formatting with screen width --- bluemix/terminal/table.go | 170 ++++++++++++++++++++++++++------- bluemix/terminal/table_test.go | 6 +- go.mod | 6 +- go.sum | 16 ++-- 4 files changed, 150 insertions(+), 48 deletions(-) diff --git a/bluemix/terminal/table.go b/bluemix/terminal/table.go index 3235e9e..95c5dc5 100644 --- a/bluemix/terminal/table.go +++ b/bluemix/terminal/table.go @@ -3,10 +3,17 @@ package terminal import ( "encoding/csv" "fmt" - "io" + "os" "strings" + "golang.org/x/term" + . "github.com/IBM-Cloud/ibm-cloud-cli-sdk/i18n" + + "io" + + "github.com/jedib0t/go-pretty/v6/table" + "github.com/jedib0t/go-pretty/v6/text" "github.com/mattn/go-runewidth" ) @@ -24,11 +31,10 @@ type Table interface { } type PrintableTable struct { - writer io.Writer - headers []string - headerPrinted bool - maxSizes []int - rows [][]string //each row is single line + writer io.Writer + headers []string + maxSizes []int + rows [][]string //each row is single line } func NewTable(w io.Writer, headers []string) Table { @@ -69,58 +75,148 @@ func (t *PrintableTable) Add(row ...string) { } } +func isWideColumn(col string) bool { + // list of common columns that are usually wide + largeColumnTypes := []string{T("ID"), T("Description")} + + for _, largeColn := range largeColumnTypes { + if strings.Contains(largeColn, col) { + return true + } + } + + return false + +} + +func terminalWidth() int { + var err error + terminalWidth, _, err := term.GetSize(int(os.Stdin.Fd())) + + if err != nil { + // Assume normal 80 char width line + terminalWidth = 80 + } + return terminalWidth +} + func (t *PrintableTable) Print() { for _, row := range append(t.rows, t.headers) { t.calculateMaxSize(row) } - if t.headerPrinted == false { - t.printHeader() - t.headerPrinted = true + tbl := table.NewWriter() + tbl.SetOutputMirror(t.writer) + tbl.SuppressTrailingSpaces() + // remove padding from the left to keep the table aligned to the left + tbl.Style().Box.PaddingLeft = "" + tbl.Style().Box.PaddingRight = strings.Repeat(" ", minSpace) + // remove all border and column and row separators + tbl.Style().Options.DrawBorder = false + tbl.Style().Options.SeparateColumns = false + tbl.Style().Options.SeparateFooter = false + tbl.Style().Options.SeparateHeader = false + tbl.Style().Options.SeparateRows = false + tbl.Style().Format.Header = text.FormatDefault + + headerRow, rows := t.createPrettyRowsAndHeaders() + columnConfig := t.createColumnConfigs() + + tbl.SetColumnConfigs(columnConfig) + tbl.AppendHeader(headerRow) + tbl.AppendRows(rows) + tbl.Render() +} + +func (t *PrintableTable) createColumnConfigs() []table.ColumnConfig { + // there must be at row in order to configure column + if len(t.rows) == 0 { + return []table.ColumnConfig{} } - for _, line := range t.rows { - t.printRow(line) + colCount := len(t.rows[0]) + var ( + widestColIndicies []int + terminalWidth = terminalWidth() + // total amount padding space that a row will take up + totalPaddingSpace = (colCount - 1) * minSpace + remainingSpace = terminalWidth - totalPaddingSpace + // the estimated max column width by dividing the remaining space evenly across the columns + maxColWidth = (terminalWidth - totalPaddingSpace) / colCount + ) + columnConfig := make([]table.ColumnConfig, len(t.maxSizes)) + + for i := range columnConfig { + columnConfig[i] = table.ColumnConfig{ + AlignHeader: text.AlignLeft, + Align: text.AlignLeft, + WidthMax: maxColWidth, + Number: i + 1, + } + + // assuming the table has headers: store columns with wide content where the max width may need to be adjusted + // using the remaining space + if t.maxSizes[i] > maxColWidth && (len(t.headers) > 0 && isWideColumn(t.headers[i])) { + widestColIndicies = append(widestColIndicies, i) + } else if t.maxSizes[i] < maxColWidth { + // use the max column width instead of the estimated max column width + // if it is shorter + columnConfig[i].WidthMax = t.maxSizes[i] + remainingSpace -= t.maxSizes[i] + } else { + remainingSpace -= maxColWidth + } } - t.rows = [][]string{} -} + // if only one wide column use the remaining space as the max column width + if len(widestColIndicies) == 1 { + idx := widestColIndicies[0] + columnConfig[idx].WidthMax = remainingSpace + } -func (t *PrintableTable) calculateMaxSize(row []string) { - for index, value := range row { - cellLength := runewidth.StringWidth(Decolorize(value)) - if t.maxSizes[index] < cellLength { - t.maxSizes[index] = cellLength + // if more than one wide column, spread the remaining space between the columns + if len(widestColIndicies) > 1 { + remainingSpace /= len(widestColIndicies) + for _, columnCfgIdx := range widestColIndicies { + columnConfig[columnCfgIdx].WidthMax = remainingSpace + } + + origRemainingSpace := remainingSpace + moreRemainingSpace := origRemainingSpace % len(widestColIndicies) + if moreRemainingSpace != 0 { + columnConfig[0].WidthMax += moreRemainingSpace } } + + return columnConfig } -func (t *PrintableTable) printHeader() { - output := "" - for col, value := range t.headers { - output = output + t.cellValue(col, HeaderColor(value)) +func (t *PrintableTable) createPrettyRowsAndHeaders() (headerRow table.Row, rows []table.Row) { + for _, header := range t.headers { + headerRow = append(headerRow, header) } - fmt.Fprintln(t.writer, output) -} -func (t *PrintableTable) printRow(row []string) { - output := "" - for columnIndex, value := range row { - if columnIndex == 0 { - value = TableContentHeaderColor(value) + for i := range t.rows { + var row table.Row + for i, cell := range t.rows[i] { + if i == 0 { + cell = TableContentHeaderColor(cell) + } + row = append(row, cell) } - - output = output + t.cellValue(columnIndex, value) + rows = append(rows, row) } - fmt.Fprintln(t.writer, output) + + return } -func (t *PrintableTable) cellValue(col int, value string) string { - padding := "" - if col < len(t.maxSizes)-1 { - padding = strings.Repeat(" ", t.maxSizes[col]-runewidth.StringWidth(Decolorize(value))+minSpace) +func (t *PrintableTable) calculateMaxSize(row []string) { + for index, value := range row { + cellLength := runewidth.StringWidth(Decolorize(value)) + if t.maxSizes[index] < cellLength { + t.maxSizes[index] = cellLength + } } - return fmt.Sprintf("%s%s", value, padding) } // Prints out a nicely/human formatted Json string instead of a table structure diff --git a/bluemix/terminal/table_test.go b/bluemix/terminal/table_test.go index 230529c..50e065a 100644 --- a/bluemix/terminal/table_test.go +++ b/bluemix/terminal/table_test.go @@ -38,7 +38,7 @@ func TestEmptyHeaderTable(t *testing.T) { testTable.Add("row1", "row2") testTable.Print() assert.Contains(t, buf.String(), "row1") - assert.Equal(t, " \nrow1 row2\n", buf.String()) + assert.Equal(t, "\nrow1 row2\n", buf.String()) } func TestEmptyHeaderTableJson(t *testing.T) { @@ -57,7 +57,7 @@ func TestZeroHeadersTable(t *testing.T) { testTable.Add("row1", "row2") testTable.Print() assert.Contains(t, buf.String(), "row1") - assert.Equal(t, "\nrow1 row2\n", buf.String()) + assert.Equal(t, "row1 row2\n", buf.String()) } func TestZeroHeadersTableJson(t *testing.T) { @@ -79,7 +79,7 @@ func TestNotEnoughRowEntires(t *testing.T) { testTable.Add("", "row2") testTable.Print() assert.Contains(t, buf.String(), "row1") - assert.Equal(t, "col1 col2\nrow1 \n row2\n", buf.String()) + assert.Equal(t, "col1 col2\nrow1\n row2\n", buf.String()) } func TestNotEnoughRowEntiresJson(t *testing.T) { diff --git a/go.mod b/go.mod index 5d94099..9e38e23 100644 --- a/go.mod +++ b/go.mod @@ -6,13 +6,14 @@ require ( github.com/fatih/color v1.7.1-0.20180516100307-2d684516a886 github.com/fatih/structs v1.0.1-0.20171020064819-f5faa72e7309 github.com/gofrs/flock v0.8.1 + github.com/jedib0t/go-pretty/v6 v6.6.1 github.com/mattn/go-colorable v0.0.0-20160210001857-9fdad7c47650 - github.com/mattn/go-runewidth v0.0.0-20151118072159-d96d1bd051f2 + github.com/mattn/go-runewidth v0.0.15 github.com/nicksnyder/go-i18n/v2 v2.2.0 github.com/onsi/gomega v1.33.0 github.com/spf13/cobra v1.6.1 github.com/spf13/pflag v1.0.5 - github.com/stretchr/testify v1.2.2 + github.com/stretchr/testify v1.8.4 golang.org/x/crypto v0.21.0 golang.org/x/text v0.14.0 gopkg.in/cheggaaa/pb.v1 v1.0.15 @@ -26,6 +27,7 @@ require ( github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/mattn/go-isatty v0.0.5-0.20180830101745-3fb116b82035 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rivo/uniseg v0.2.0 // indirect golang.org/x/net v0.23.0 // indirect golang.org/x/sys v0.18.0 // indirect golang.org/x/term v0.18.0 // indirect diff --git a/go.sum b/go.sum index 014842c..34ba4ae 100644 --- a/go.sum +++ b/go.sum @@ -16,16 +16,18 @@ github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= -github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y= +github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jedib0t/go-pretty/v6 v6.6.1 h1:iJ65Xjb680rHcikRj6DSIbzCex2huitmc7bDtxYVWyc= +github.com/jedib0t/go-pretty/v6 v6.6.1/go.mod h1:zbn98qrYlh95FIhwwsbIip0LYpwSG8SUOScs+v9/t0E= github.com/mattn/go-colorable v0.0.0-20160210001857-9fdad7c47650 h1:pwtfAm8Do0gwFJ2J+iUrEVR9qI03BpDSuDQCIqbd6iY= github.com/mattn/go-colorable v0.0.0-20160210001857-9fdad7c47650/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.5-0.20180830101745-3fb116b82035 h1:USWjF42jDCSEeikX/G1g40ZWnsPXN5WkZ4jMHZWyBK4= github.com/mattn/go-isatty v0.0.5-0.20180830101745-3fb116b82035/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-runewidth v0.0.0-20151118072159-d96d1bd051f2 h1:K4BQSf+ZGZ8QlDL8RsUD1DES25Lgetj1JJGJz1G7Bno= -github.com/mattn/go-runewidth v0.0.0-20151118072159-d96d1bd051f2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/nicksnyder/go-i18n/v2 v2.2.0 h1:MNXbyPvd141JJqlU6gJKrczThxJy+kdCNivxZpBQFkw= github.com/nicksnyder/go-i18n/v2 v2.2.0/go.mod h1:4OtLfzqyAxsscyCb//3gfqSvBc81gImX91LrZzczN1o= github.com/onsi/ginkgo/v2 v2.17.1 h1:V++EzdbhI4ZV4ev0UTIj0PzhzOcReJFyJaLjtSF55M8= @@ -34,13 +36,15 @@ github.com/onsi/gomega v1.33.0 h1:snPCflnZrpMsy94p4lXVEkHo12lmPnc3vY5XBbreexE= github.com/onsi/gomega v1.33.0/go.mod h1:+925n5YtiFsLzzafLUHzVMBpvvRAzrydIBiSIxjX3wY= 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/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=