go.charczuk.com@v0.0.0-20240327042549-bc490516bd1a/sdk/ansi/table.go (about) 1 /* 2 3 Copyright (c) 2023 - Present. Will Charczuk. All rights reserved. 4 Use of this source code is governed by a MIT license that can be found in the LICENSE file at the root of the repository. 5 6 */ 7 8 package ansi 9 10 import ( 11 "errors" 12 "fmt" 13 "io" 14 "reflect" 15 "strings" 16 "unicode" 17 "unicode/utf8" 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 errors.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 var columns []string 56 for index := 0; index < ct.NumField(); index++ { 57 name := ct.Field(index).Name 58 if name == "" || unicode.IsLower(firstRune(name)) { 59 continue 60 } 61 columns = append(columns, name) 62 } 63 64 var rows [][]string 65 var rowValue reflect.Value 66 var field reflect.Value 67 for row := 0; row < cv.Len(); row++ { 68 rowValue = cv.Index(row) 69 rowValues := make([]string, len(columns)) 70 for columnIndex := 0; columnIndex < len(columns); columnIndex++ { 71 field = rowValue.FieldByName(columns[columnIndex]) 72 rowValues[columnIndex] = fmt.Sprintf("%v", field.Interface()) 73 } 74 rows = append(rows, rowValues) 75 } 76 77 return Table(wr, columns, rows) 78 } 79 80 func firstRune(str string) (out rune) { 81 if str == "" { 82 return 83 } 84 for _, out = range str { 85 break 86 } 87 return 88 } 89 90 // Table writes a table to a given writer. 91 func Table(wr io.Writer, columns []string, rows [][]string) (err error) { 92 if len(columns) == 0 { 93 return errors.New("table; invalid columns; column set is empty") 94 } 95 96 /* helpers */ 97 defer func() { 98 if r := recover(); r != nil { 99 if typed, ok := r.(error); ok { 100 err = typed 101 } 102 } 103 }() 104 write := func(str string) { 105 _, writeErr := io.WriteString(wr, str) 106 if writeErr != nil { 107 panic(writeErr) 108 } 109 } 110 /* end helpers */ 111 112 /* begin establish max widths of columns */ 113 maxWidths := make([]int, len(columns)) 114 for index, columnName := range columns { 115 maxWidths[index] = stringWidth(columnName) 116 } 117 118 var width int 119 for _, cols := range rows { 120 for index, columnValue := range cols { 121 width = stringWidth(columnValue) 122 if maxWidths[index] < width { 123 maxWidths[index] = width 124 } 125 } 126 } 127 /* end establish max widths of columns */ 128 129 /* draw top of column row */ 130 write(TableTopLeft) 131 for index := range columns { 132 write(repeat(TableHorizBar, maxWidths[index])) 133 if isNotLast(index, columns) { 134 write(TableTopSep) 135 } 136 } 137 write(TableTopRight) 138 write(NewLine) 139 /* end draw top of column row */ 140 141 /* draw column names */ 142 write(TableVertBar) 143 for index, columnLabel := range columns { 144 write(padRight(columnLabel, maxWidths[index])) 145 if isNotLast(index, columns) { 146 write(TableVertBar) 147 } 148 } 149 write(TableVertBar) 150 write(NewLine) 151 /* end draw column names */ 152 153 /* draw bottom of column row */ 154 write(TableMidLeft) 155 for index := range columns { 156 write(repeat(TableHorizBar, maxWidths[index])) 157 if isNotLast(index, columns) { 158 write(TableMidSep) 159 } 160 } 161 write(TableMidRight) 162 write(NewLine) 163 /* end draw bottom of column row */ 164 165 /* draw rows */ 166 for _, row := range rows { 167 write(TableVertBar) 168 for index, column := range row { 169 write(padRight(column, maxWidths[index])) 170 if isNotLast(index, columns) { 171 write(TableVertBar) 172 } 173 } 174 write(TableVertBar) 175 write(NewLine) 176 } 177 /* end draw rows */ 178 179 /* draw footer */ 180 write(TableBottomLeft) 181 for index := range columns { 182 write(repeat(TableHorizBar, maxWidths[index])) 183 if isNotLast(index, columns) { 184 write(TableBottomSep) 185 } 186 } 187 write(TableBottomRight) 188 write(NewLine) 189 /* end draw footer */ 190 return 191 } 192 193 func stringWidth(value string) (width int) { 194 var runeWidth int 195 for _, c := range value { 196 runeWidth = utf8.RuneLen(c) 197 if runeWidth > 1 { 198 width += 2 199 } else { 200 width++ 201 } 202 } 203 return 204 } 205 206 func repeat(str string, count int) string { 207 return strings.Repeat(str, count) 208 } 209 210 func padRight(value string, width int) string { 211 valueWidth := stringWidth(value) 212 spaces := width - valueWidth 213 if spaces == 0 { 214 return value 215 } 216 return value + strings.Repeat(" ", spaces) 217 } 218 219 func isNotLast(index int, values []string) bool { 220 return index < (len(values) - 1) 221 }