github.com/blend/go-sdk@v1.20220411.3/ansi/table.go (about) 1 /* 2 3 Copyright (c) 2022 - Present. Blend Labs, Inc. All rights reserved 4 Use of this source code is governed by a MIT license that can be found in the LICENSE file. 5 6 */ 7 8 package ansi 9 10 import ( 11 "fmt" 12 "io" 13 "reflect" 14 "strings" 15 "unicode/utf8" 16 17 "github.com/blend/go-sdk/ex" 18 ) 19 20 // Table character constants. 21 const ( 22 TableTopLeft = "┌" 23 TableTopRight = "┐" 24 TableBottomLeft = "└" 25 TableBottomRight = "┘" 26 TableMidLeft = "├" 27 TableMidRight = "┤" 28 TableVertBar = "│" 29 TableHorizBar = "─" 30 TableTopSep = "┬" 31 TableBottomSep = "┴" 32 TableMidSep = "┼" 33 NewLine = "\n" 34 ) 35 36 // TableForSlice prints a table for a given slice. 37 // It will infer column names from the struct fields. 38 // If it is a mixed array (i.e. []interface{}) it will probably panic. 39 func TableForSlice(wr io.Writer, collection interface{}) error { 40 // infer the column names from the fields 41 cv := reflect.ValueOf(collection) 42 for cv.Kind() == reflect.Ptr { 43 cv = cv.Elem() 44 } 45 46 if cv.Kind() != reflect.Slice { 47 return ex.New("table for slice; cannot iterate over non-slice collection") 48 } 49 50 ct := cv.Type() 51 for ct.Kind() == reflect.Ptr || ct.Kind() == reflect.Slice { 52 ct = ct.Elem() 53 } 54 55 columns := make([]string, ct.NumField()) 56 for index := 0; index < ct.NumField(); index++ { 57 columns[index] = ct.Field(index).Name 58 } 59 60 var rows [][]string 61 var rowValue reflect.Value 62 for row := 0; row < cv.Len(); row++ { 63 rowValue = cv.Index(row) 64 rowValues := make([]string, ct.NumField()) 65 for fieldIndex := 0; fieldIndex < ct.NumField(); fieldIndex++ { 66 rowValues[fieldIndex] = fmt.Sprintf("%v", rowValue.Field(fieldIndex).Interface()) 67 } 68 rows = append(rows, rowValues) 69 } 70 71 return Table(wr, columns, rows) 72 } 73 74 // Table writes a table to a given writer. 75 func Table(wr io.Writer, columns []string, rows [][]string) (err error) { 76 if len(columns) == 0 { 77 return ex.New("table; invalid columns; column set is empty") 78 } 79 80 /* helpers */ 81 defer func() { 82 if r := recover(); r != nil { 83 if typed, ok := r.(error); ok { 84 err = typed 85 } 86 } 87 }() 88 write := func(str string) { 89 _, writeErr := io.WriteString(wr, str) 90 if writeErr != nil { 91 panic(writeErr) 92 } 93 } 94 /* end helpers */ 95 96 /* begin establish max widths of columns */ 97 maxWidths := make([]int, len(columns)) 98 for index, columnName := range columns { 99 maxWidths[index] = stringWidth(columnName) 100 } 101 102 var width int 103 for _, cols := range rows { 104 for index, columnValue := range cols { 105 width = stringWidth(columnValue) 106 if maxWidths[index] < width { 107 maxWidths[index] = width 108 } 109 } 110 } 111 /* end establish max widths of columns */ 112 113 /* draw top of column row */ 114 write(TableTopLeft) 115 for index := range columns { 116 write(repeat(TableHorizBar, maxWidths[index])) 117 if isNotLast(index, columns) { 118 write(TableTopSep) 119 } 120 } 121 write(TableTopRight) 122 write(NewLine) 123 /* end draw top of column row */ 124 125 /* draw column names */ 126 write(TableVertBar) 127 for index, columnLabel := range columns { 128 write(padRight(columnLabel, maxWidths[index])) 129 if isNotLast(index, columns) { 130 write(TableVertBar) 131 } 132 } 133 write(TableVertBar) 134 write(NewLine) 135 /* end draw column names */ 136 137 /* draw bottom of column row */ 138 write(TableMidLeft) 139 for index := range columns { 140 write(repeat(TableHorizBar, maxWidths[index])) 141 if isNotLast(index, columns) { 142 write(TableMidSep) 143 } 144 } 145 write(TableMidRight) 146 write(NewLine) 147 /* end draw bottom of column row */ 148 149 /* draw rows */ 150 for _, row := range rows { 151 write(TableVertBar) 152 for index, column := range row { 153 write(padRight(column, maxWidths[index])) 154 if isNotLast(index, columns) { 155 write(TableVertBar) 156 } 157 } 158 write(TableVertBar) 159 write(NewLine) 160 } 161 /* end draw rows */ 162 163 /* draw footer */ 164 write(TableBottomLeft) 165 for index := range columns { 166 write(repeat(TableHorizBar, maxWidths[index])) 167 if isNotLast(index, columns) { 168 write(TableBottomSep) 169 } 170 } 171 write(TableBottomRight) 172 write(NewLine) 173 /* end draw footer */ 174 return 175 } 176 177 func stringWidth(value string) (width int) { 178 var runeWidth int 179 for _, c := range value { 180 runeWidth = utf8.RuneLen(c) 181 if runeWidth > 1 { 182 width += 2 183 } else { 184 width++ 185 } 186 } 187 return 188 } 189 190 func repeat(str string, count int) string { 191 return strings.Repeat(str, count) 192 } 193 194 func padRight(value string, width int) string { 195 valueWidth := stringWidth(value) 196 spaces := width - valueWidth 197 if spaces == 0 { 198 return value 199 } 200 return value + strings.Repeat(" ", spaces) 201 } 202 203 func isNotLast(index int, values []string) bool { 204 return index < (len(values) - 1) 205 }