github.com/andrewhsu/cli/v2@v2.0.1-0.20210910131313-d4b4061f5b89/utils/table_printer.go (about) 1 package utils 2 3 import ( 4 "fmt" 5 "io" 6 "sort" 7 "strings" 8 9 "github.com/andrewhsu/cli/v2/pkg/iostreams" 10 "github.com/andrewhsu/cli/v2/pkg/text" 11 ) 12 13 type TablePrinter interface { 14 IsTTY() bool 15 AddField(string, func(int, string) string, func(string) string) 16 EndRow() 17 Render() error 18 } 19 20 type TablePrinterOptions struct { 21 IsTTY bool 22 } 23 24 func NewTablePrinter(io *iostreams.IOStreams) TablePrinter { 25 return NewTablePrinterWithOptions(io, TablePrinterOptions{ 26 IsTTY: io.IsStdoutTTY(), 27 }) 28 } 29 30 func NewTablePrinterWithOptions(io *iostreams.IOStreams, opts TablePrinterOptions) TablePrinter { 31 if opts.IsTTY { 32 var maxWidth int 33 if io.IsStdoutTTY() { 34 maxWidth = io.TerminalWidth() 35 } else { 36 maxWidth = io.ProcessTerminalWidth() 37 } 38 return &ttyTablePrinter{ 39 out: io.Out, 40 maxWidth: maxWidth, 41 } 42 } 43 return &tsvTablePrinter{ 44 out: io.Out, 45 } 46 } 47 48 type tableField struct { 49 Text string 50 TruncateFunc func(int, string) string 51 ColorFunc func(string) string 52 } 53 54 func (f *tableField) DisplayWidth() int { 55 return text.DisplayWidth(f.Text) 56 } 57 58 type ttyTablePrinter struct { 59 out io.Writer 60 maxWidth int 61 rows [][]tableField 62 } 63 64 func (t ttyTablePrinter) IsTTY() bool { 65 return true 66 } 67 68 func (t *ttyTablePrinter) AddField(s string, truncateFunc func(int, string) string, colorFunc func(string) string) { 69 if truncateFunc == nil { 70 truncateFunc = text.Truncate 71 } 72 if t.rows == nil { 73 t.rows = make([][]tableField, 1) 74 } 75 rowI := len(t.rows) - 1 76 field := tableField{ 77 Text: s, 78 TruncateFunc: truncateFunc, 79 ColorFunc: colorFunc, 80 } 81 t.rows[rowI] = append(t.rows[rowI], field) 82 } 83 84 func (t *ttyTablePrinter) EndRow() { 85 t.rows = append(t.rows, []tableField{}) 86 } 87 88 func (t *ttyTablePrinter) Render() error { 89 if len(t.rows) == 0 { 90 return nil 91 } 92 93 delim := " " 94 numCols := len(t.rows[0]) 95 colWidths := t.calculateColumnWidths(len(delim)) 96 97 for _, row := range t.rows { 98 for col, field := range row { 99 if col > 0 { 100 _, err := fmt.Fprint(t.out, delim) 101 if err != nil { 102 return err 103 } 104 } 105 truncVal := field.TruncateFunc(colWidths[col], field.Text) 106 if col < numCols-1 { 107 // pad value with spaces on the right 108 if padWidth := colWidths[col] - field.DisplayWidth(); padWidth > 0 { 109 truncVal += strings.Repeat(" ", padWidth) 110 } 111 } 112 if field.ColorFunc != nil { 113 truncVal = field.ColorFunc(truncVal) 114 } 115 _, err := fmt.Fprint(t.out, truncVal) 116 if err != nil { 117 return err 118 } 119 } 120 if len(row) > 0 { 121 _, err := fmt.Fprint(t.out, "\n") 122 if err != nil { 123 return err 124 } 125 } 126 } 127 return nil 128 } 129 130 func (t *ttyTablePrinter) calculateColumnWidths(delimSize int) []int { 131 numCols := len(t.rows[0]) 132 allColWidths := make([][]int, numCols) 133 for _, row := range t.rows { 134 for col, field := range row { 135 allColWidths[col] = append(allColWidths[col], field.DisplayWidth()) 136 } 137 } 138 139 // calculate max & median content width per column 140 maxColWidths := make([]int, numCols) 141 // medianColWidth := make([]int, numCols) 142 for col := 0; col < numCols; col++ { 143 widths := allColWidths[col] 144 sort.Ints(widths) 145 maxColWidths[col] = widths[len(widths)-1] 146 // medianColWidth[col] = widths[(len(widths)+1)/2] 147 } 148 149 colWidths := make([]int, numCols) 150 // never truncate the first column 151 colWidths[0] = maxColWidths[0] 152 // never truncate the last column if it contains URLs 153 if strings.HasPrefix(t.rows[0][numCols-1].Text, "https://") { 154 colWidths[numCols-1] = maxColWidths[numCols-1] 155 } 156 157 availWidth := func() int { 158 setWidths := 0 159 for col := 0; col < numCols; col++ { 160 setWidths += colWidths[col] 161 } 162 return t.maxWidth - delimSize*(numCols-1) - setWidths 163 } 164 numFixedCols := func() int { 165 fixedCols := 0 166 for col := 0; col < numCols; col++ { 167 if colWidths[col] > 0 { 168 fixedCols++ 169 } 170 } 171 return fixedCols 172 } 173 174 // set the widths of short columns 175 if w := availWidth(); w > 0 { 176 if numFlexColumns := numCols - numFixedCols(); numFlexColumns > 0 { 177 perColumn := w / numFlexColumns 178 for col := 0; col < numCols; col++ { 179 if max := maxColWidths[col]; max < perColumn { 180 colWidths[col] = max 181 } 182 } 183 } 184 } 185 186 firstFlexCol := -1 187 // truncate long columns to the remaining available width 188 if numFlexColumns := numCols - numFixedCols(); numFlexColumns > 0 { 189 perColumn := availWidth() / numFlexColumns 190 for col := 0; col < numCols; col++ { 191 if colWidths[col] == 0 { 192 if firstFlexCol == -1 { 193 firstFlexCol = col 194 } 195 if max := maxColWidths[col]; max < perColumn { 196 colWidths[col] = max 197 } else { 198 colWidths[col] = perColumn 199 } 200 } 201 } 202 } 203 204 // add remainder to the first flex column 205 if w := availWidth(); w > 0 && firstFlexCol > -1 { 206 colWidths[firstFlexCol] += w 207 if max := maxColWidths[firstFlexCol]; max < colWidths[firstFlexCol] { 208 colWidths[firstFlexCol] = max 209 } 210 } 211 212 return colWidths 213 } 214 215 type tsvTablePrinter struct { 216 out io.Writer 217 currentCol int 218 } 219 220 func (t tsvTablePrinter) IsTTY() bool { 221 return false 222 } 223 224 func (t *tsvTablePrinter) AddField(text string, _ func(int, string) string, _ func(string) string) { 225 if t.currentCol > 0 { 226 fmt.Fprint(t.out, "\t") 227 } 228 fmt.Fprint(t.out, text) 229 t.currentCol++ 230 } 231 232 func (t *tsvTablePrinter) EndRow() { 233 fmt.Fprint(t.out, "\n") 234 t.currentCol = 0 235 } 236 237 func (t *tsvTablePrinter) Render() error { 238 return nil 239 }