├── .gitignore ├── README.md ├── example └── main.go ├── go.mod ├── writer.go └── writer_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tablewr 2 | 3 | ## A very minimal table writer in Golang 4 | 5 | - Uses the stdlib's `text/tabwriter` to render a table 6 | - Auto-scales column widths based on the column's maximum length 7 | - Tiny API 8 | 9 | ### Usage 10 | 11 | ```go 12 | package main 13 | 14 | import ( 15 | "github.com/shubhang93/tablewr" 16 | "os" 17 | ) 18 | 19 | func main() { 20 | wr := tablewr.New(os.Stderr, 0, tablewr.WithSep()) 21 | data := [][]string{ 22 | {"title", "price", "sold out", "rating"}, 23 | {"The Shining", "30$", "yes", "*****"}, 24 | {"The Mask", "10$", "no", "***"}, 25 | {"Godfather", "40$", "no", "*****"}, 26 | {"Godfather-2", "40$", "no", "*****"}, 27 | {"Shawshank Redemption", "30$", "yes", "*****"}, 28 | } 29 | if err := wr.Write(data); err != nil { 30 | panic("write err:" + err.Error()) 31 | } 32 | } 33 | 34 | ``` 35 | 36 | ### Output 37 | 38 | ```text 39 | 40 | title | price | sold out | rating | 41 | ----------------------|-------|----------|--------| 42 | The Shining | 30$ | yes | ***** | 43 | The Mask | 10$ | no | *** | 44 | Godfather | 40$ | no | ***** | 45 | Godfather-2 | 40$ | no | ***** | 46 | Shawshank Redemption | 30$ | yes | ***** | 47 | 48 | 49 | ``` 50 | 51 | -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | 3 | package main 4 | 5 | import ( 6 | "github.com/shubhang93/tablewr" 7 | "os" 8 | ) 9 | 10 | func main() { 11 | wr := tablewr.New(os.Stdout, 0, tablewr.WithSep()) 12 | data := [][]string{ 13 | {"title", "price", "sold out", "rating"}, 14 | {"The Shining", "30$", "yes", "*****"}, 15 | {"The Mask", "10$", "no", "***"}, 16 | {"Godfather", "40$", "no", "*****"}, 17 | {"Godfather-2", "40$", "no", "*****"}, 18 | {"Shawshank Redemption", "30$", "yes", "*****"}, 19 | } 20 | if err := wr.Write(data); err != nil { 21 | panic("write err:" + err.Error()) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/shubhang93/tablewr 2 | 3 | go 1.22.5 4 | -------------------------------------------------------------------------------- /writer.go: -------------------------------------------------------------------------------- 1 | package tablewr 2 | 3 | import ( 4 | "io" 5 | "strings" 6 | "text/tabwriter" 7 | ) 8 | 9 | type TableWriter struct { 10 | wr *tabwriter.Writer 11 | headerRowIndex int 12 | tableWriterOpts 13 | } 14 | 15 | type tableWriterOpts struct { 16 | TableTopPadding int 17 | TableBottomPadding int 18 | ColLeftPadding int 19 | ColRightPadding int 20 | OptsFlags uint 21 | } 22 | 23 | var defaultOptions = tableWriterOpts{ 24 | TableTopPadding: 1, 25 | TableBottomPadding: 1, 26 | ColLeftPadding: 1, 27 | ColRightPadding: 1, 28 | } 29 | 30 | func WithSep() func(opts *tableWriterOpts) { 31 | return func(opts *tableWriterOpts) { 32 | opts.OptsFlags |= tabwriter.Debug 33 | } 34 | } 35 | 36 | func WithColPadding(left, right int) func(opts *tableWriterOpts) { 37 | return func(opts *tableWriterOpts) { 38 | opts.ColLeftPadding = left 39 | opts.ColRightPadding = right 40 | } 41 | } 42 | 43 | func WithTablePadding(top, bottom int) func(opts *tableWriterOpts) { 44 | return func(opts *tableWriterOpts) { 45 | opts.TableTopPadding = top 46 | opts.TableBottomPadding = bottom 47 | } 48 | } 49 | 50 | func New(writer io.Writer, headerRowIndex int, opts ...func(opts *tableWriterOpts)) *TableWriter { 51 | tropts := defaultOptions 52 | for _, opt := range opts { 53 | opt(&tropts) 54 | } 55 | tr := tabwriter.NewWriter(writer, 0, 0, 0, '\t', tropts.OptsFlags) 56 | return &TableWriter{ 57 | wr: tr, 58 | headerRowIndex: headerRowIndex, 59 | tableWriterOpts: tropts, 60 | } 61 | } 62 | 63 | func (twr *TableWriter) Write(rows [][]string) error { 64 | if len(rows) < 1 { 65 | return nil 66 | } 67 | cols := rows[0] 68 | if len(cols) == 0 { 69 | return nil 70 | } 71 | 72 | maxWidths := make([]int, len(cols)) 73 | for _, row := range rows { 74 | for i, col := range row { 75 | maxWidths[i] = max(maxWidths[i], len(col)) 76 | } 77 | } 78 | 79 | defer twr.wr.Flush() 80 | 81 | if _, err := io.WriteString(twr.wr, strings.Repeat("\n", twr.TableTopPadding)); err != nil { 82 | return err 83 | } 84 | leftPadding := strings.Repeat(" ", twr.ColLeftPadding) 85 | bottomPadding := strings.Repeat("\n", twr.TableBottomPadding) 86 | 87 | for i, row := range rows { 88 | for j, col := range row { 89 | mw := maxWidths[j] 90 | numLeftOver := mw - len(col) 91 | rightPadding := strings.Repeat(" ", numLeftOver+twr.ColRightPadding) 92 | 93 | if _, err := io.WriteString(twr.wr, leftPadding); err != nil { 94 | return err 95 | } 96 | if _, err := io.WriteString(twr.wr, col); err != nil { 97 | return err 98 | } 99 | if _, err := io.WriteString(twr.wr, rightPadding); err != nil { 100 | return err 101 | } 102 | if _, err := io.WriteString(twr.wr, "\t"); err != nil { 103 | return err 104 | } 105 | } 106 | 107 | _, err := io.WriteString(twr.wr, "\n") 108 | if err != nil { 109 | return err 110 | } 111 | if i == twr.headerRowIndex { 112 | if err := twr.writeRowDelim(maxWidths, twr.ColRightPadding+twr.ColRightPadding); err != nil { 113 | return err 114 | } 115 | } 116 | } 117 | 118 | if _, err := io.WriteString(twr.wr, bottomPadding); err != nil { 119 | return err 120 | } 121 | return nil 122 | } 123 | 124 | func (twr *TableWriter) writeRowDelim(colWidths []int, padding int) error { 125 | for _, width := range colWidths { 126 | if _, err := io.WriteString(twr.wr, strings.Repeat("-", width)); err != nil { 127 | return err 128 | } 129 | if _, err := io.WriteString(twr.wr, strings.Repeat("-", padding)); err != nil { 130 | return err 131 | } 132 | 133 | if _, err := io.WriteString(twr.wr, "\t"); err != nil { 134 | return err 135 | } 136 | } 137 | if _, err := io.WriteString(twr.wr, "\n"); err != nil { 138 | return err 139 | } 140 | return nil 141 | } 142 | -------------------------------------------------------------------------------- /writer_test.go: -------------------------------------------------------------------------------- 1 | package tablewr 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | ) 8 | 9 | func TestTableWriter_Write(t *testing.T) { 10 | 11 | tests := []struct { 12 | Name string 13 | Expected string 14 | Options []func(o *tableWriterOpts) 15 | }{{Name: "default settings", 16 | Expected: "\n name age city \n---------------------\n foo 23 blr \n bar 25 france \n foobar 100 ohio \n\n", 17 | Options: nil}, 18 | { 19 | Name: "with options", 20 | Expected: "\n\n name | age | city |\n----------|-------|----------|\n foo | 23 | blr |\n bar | 25 | france |\n foobar | 100 | ohio |\n\n\n", 21 | Options: []func(o *tableWriterOpts){WithSep(), WithTablePadding(2, 2), WithColPadding(2, 2)}, 22 | }} 23 | 24 | for _, test := range tests { 25 | var out bytes.Buffer 26 | tr := New(&out, 0, test.Options...) 27 | rows := [][]string{{"name", "age", "city"}, 28 | {"foo", "23", "blr"}, 29 | {"bar", "25", "france"}, 30 | {"foobar", "100", "ohio"}} 31 | 32 | if err := tr.Write(rows); err != nil { 33 | t.Error(err) 34 | return 35 | } 36 | 37 | if test.Expected != out.String() { 38 | t.Errorf("want:\n%q \n got:\n%q", test.Expected, out.String()) 39 | } 40 | } 41 | } 42 | 43 | func BenchmarkTableWriter_Write(b *testing.B) { 44 | sizes := []int{ 45 | 10, 46 | 100, 47 | 1000, 48 | 10000, 49 | 100000, 50 | } 51 | for _, size := range sizes { 52 | b.Run(fmt.Sprintf("size:%d", size), func(b *testing.B) { 53 | b.ReportAllocs() 54 | rows := makeRowsWithHeader(size) 55 | var buff bytes.Buffer 56 | tr := New(&buff, 0, WithSep()) 57 | if err := tr.Write(rows); err != nil { 58 | return 59 | } 60 | }) 61 | } 62 | } 63 | 64 | func makeRowsWithHeader(size int) [][]string { 65 | rows := make([][]string, size+1) 66 | for i := range rows { 67 | rows[i] = []string{"data", "dataa", "dataaa", "dataaaa", "dataaaaaa"} 68 | } 69 | rows[0] = []string{"header", "headere", "headerrr", "headerrrrr", "headerrrrrrr"} 70 | return rows 71 | } 72 | --------------------------------------------------------------------------------