Skip to content

Commit

Permalink
feat: used go-pretty to improved table formatting with screen width
Browse files Browse the repository at this point in the history
  • Loading branch information
Aerex committed Nov 11, 2024
1 parent 9a70219 commit 0fa5825
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 48 deletions.
170 changes: 133 additions & 37 deletions bluemix/terminal/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions bluemix/terminal/table_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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) {
Expand All @@ -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) {
Expand Down
6 changes: 4 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
16 changes: 10 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand All @@ -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=
Expand Down

0 comments on commit 0fa5825

Please sign in to comment.