github.com/mook-as/cf-cli@v7.0.0-beta.28.0.20200120190804-b91c115fae48+incompatible/cf/terminal/table.go (about) 1 package terminal 2 3 import ( 4 "bytes" 5 "fmt" 6 "io" 7 "strings" 8 ) 9 10 // PrintableTable is an implementation of the Table interface. It 11 // remembers the headers, the added rows, the column widths, and a 12 // number of other things. 13 type Table struct { 14 ui UI 15 headers []string 16 headerPrinted bool 17 columnWidth []int 18 rowHeight []int 19 rows [][]string 20 colSpacing string 21 transformer []Transformer 22 } 23 24 // Transformer is the type of functions used to modify the content of 25 // a table cell for actual display. For multi-line content of a cell 26 // the transformation is applied to each individual line. 27 type Transformer func(s string) string 28 29 // NewTable is the constructor function creating a new printable table 30 // from a list of headers. The table is also connected to a UI, which 31 // is where it will print itself to on demand. 32 func NewTable(headers []string) *Table { 33 pt := &Table{ 34 headers: headers, 35 columnWidth: make([]int, len(headers)), 36 colSpacing: " ", 37 transformer: make([]Transformer, len(headers)), 38 } 39 // Standard colorization, column 0 is auto-highlighted as some 40 // name. Everything else has no transformation (== identity 41 // transform) 42 for i := range pt.transformer { 43 pt.transformer[i] = nop 44 } 45 if 0 < len(headers) { 46 pt.transformer[0] = TableContentHeaderColor 47 } 48 return pt 49 } 50 51 // NoHeaders disables the printing of the header row for the specified 52 // table. 53 func (t *Table) NoHeaders() { 54 // Fake the Print() code into the belief that the headers have 55 // been printed already. 56 t.headerPrinted = true 57 } 58 59 // SetTransformer specifies a string transformer to apply to the 60 // content of the given column in the specified table. 61 func (t *Table) SetTransformer(columnIndex int, tr Transformer) { 62 t.transformer[columnIndex] = tr 63 } 64 65 // Add extends the table by another row. 66 func (t *Table) Add(row ...string) { 67 t.rows = append(t.rows, row) 68 } 69 70 // PrintTo is the core functionality for printing the table, placing 71 // the formatted table into the writer given to it as argument. The 72 // exported Print() is just a wrapper around this which redirects the 73 // result into CF data structures. 74 // Once a table has been printed onto a Writer, it cannot be printed 75 // again. 76 func (t *Table) PrintTo(result io.Writer) error { 77 t.rowHeight = make([]int, len(t.rows)+1) 78 79 rowIndex := 0 80 if !t.headerPrinted { 81 // row transformer header row 82 err := t.calculateMaxSize(transHeader, rowIndex, t.headers) 83 if err != nil { 84 return err 85 } 86 rowIndex++ 87 } 88 89 for _, row := range t.rows { 90 // table is row transformer itself, for content rows 91 err := t.calculateMaxSize(t, rowIndex, row) 92 if err != nil { 93 return err 94 } 95 rowIndex++ 96 } 97 98 rowIndex = 0 99 if !t.headerPrinted { 100 err := t.printRow(result, transHeader, rowIndex, t.headers) 101 if err != nil { 102 return err 103 } 104 t.headerPrinted = true 105 rowIndex++ 106 } 107 108 for row := range t.rows { 109 err := t.printRow(result, t, rowIndex, t.rows[row]) 110 if err != nil { 111 return err 112 } 113 rowIndex++ 114 } 115 116 t.rows = [][]string{} 117 return nil 118 } 119 120 // calculateMaxSize iterates over the collected rows of the specified 121 // table, and their strings, determining the height of each row (in 122 // lines), and the width of each column (in characters). The results 123 // are stored in the table for use by Print. 124 func (t *Table) calculateMaxSize(transformer rowTransformer, rowIndex int, row []string) error { 125 126 // Iterate columns 127 for columnIndex := range row { 128 // Truncate long row, ignore the additional fields. 129 if columnIndex >= len(t.headers) { 130 break 131 } 132 133 // Note that the length of the cell in characters is 134 // __not__ equivalent to its width. Because it may be 135 // a multi-line value. We have to split the cell into 136 // lines and check the width of each such fragment. 137 // The number of lines founds also goes into the row 138 // height. 139 140 lines := strings.Split(row[columnIndex], "\n") 141 height := len(lines) 142 143 if t.rowHeight[rowIndex] < height { 144 t.rowHeight[rowIndex] = height 145 } 146 147 for i := range lines { 148 // (**) See also 'printCellValue' (pCV). Here 149 // and there we have to apply identical 150 // transformations to the cell value to get 151 // matching cell width information. If they do 152 // not match then pCV may compute a cell width 153 // larger than the max width found here, a 154 // negative padding length from that, and 155 // subsequently return an error. What 156 // was further missing is trimming before 157 // entering the user-transform. Especially 158 // with color transforms any trailing space 159 // going in will not be removable for print. 160 // 161 // This happened for 162 // https://www.pivotaltracker.com/n/projects/892938/stories/117404629 163 164 value := trim(Decolorize(transformer.Transform(columnIndex, trim(lines[i])))) 165 width, err := visibleSize(value) 166 if err != nil { 167 return err 168 } 169 if t.columnWidth[columnIndex] < width { 170 t.columnWidth[columnIndex] = width 171 } 172 } 173 } 174 return nil 175 } 176 177 // printRow is responsible for the layouting, transforming and 178 // printing of the string in a single row 179 func (t *Table) printRow(result io.Writer, transformer rowTransformer, rowIndex int, row []string) error { 180 181 height := t.rowHeight[rowIndex] 182 183 // Compute the index of the last column as the min number of 184 // cells in the header and cells in the current row. 185 // Note: math.Min seems to be for float only :( 186 last := len(t.headers) - 1 187 lastr := len(row) - 1 188 if lastr < last { 189 last = lastr 190 } 191 192 // Note how we always print into a line buffer before placing 193 // the assembled line into the result. This allows us to trim 194 // superfluous trailing whitespace from the line before making 195 // it final. 196 197 if height <= 1 { 198 // Easy case, all cells in the row are single-line 199 line := &bytes.Buffer{} 200 201 for columnIndex := range row { 202 // Truncate long row, ignore the additional fields. 203 if columnIndex >= len(t.headers) { 204 break 205 } 206 207 err := t.printCellValue(line, transformer, columnIndex, last, row[columnIndex]) 208 if err != nil { 209 return err 210 } 211 } 212 213 fmt.Fprintf(result, "%s\n", trim(string(line.Bytes()))) 214 return nil 215 } 216 217 // We have at least one multi-line cell in this row. 218 // Treat it a bit like a mini-table. 219 220 // Step I. Fill the mini-table. Note how it is stored 221 // column-major, not row-major. 222 223 // [column][row]string 224 sub := make([][]string, len(t.headers)) 225 for columnIndex := range row { 226 // Truncate long row, ignore the additional fields. 227 if columnIndex >= len(t.headers) { 228 break 229 } 230 sub[columnIndex] = strings.Split(row[columnIndex], "\n") 231 // (*) Extend the column to the full height. 232 for len(sub[columnIndex]) < height { 233 sub[columnIndex] = append(sub[columnIndex], "") 234 } 235 } 236 237 // Step II. Iterate over the rows, then columns to 238 // collect the output. This assumes that all 239 // the rows in sub are the same height. See 240 // (*) above where that is made true. 241 242 for rowIndex := range sub[0] { 243 line := &bytes.Buffer{} 244 245 for columnIndex := range sub { 246 err := t.printCellValue(line, transformer, columnIndex, last, sub[columnIndex][rowIndex]) 247 if err != nil { 248 return err 249 } 250 } 251 252 fmt.Fprintf(result, "%s\n", trim(string(line.Bytes()))) 253 } 254 return nil 255 } 256 257 // printCellValue pads the specified string to the width of the given 258 // column, adds the spacing bewtween columns, and returns the result. 259 func (t *Table) printCellValue(result io.Writer, transformer rowTransformer, col, last int, value string) error { 260 value = trim(transformer.Transform(col, trim(value))) 261 fmt.Fprint(result, value) 262 263 // Pad all columns, but the last in this row (with the size of 264 // the header row limiting this). This ensures that most of 265 // the irrelevant spacing is not printed. At the moment 266 // irrelevant spacing can only occur when printing a row with 267 // multi-line cells, introducing a physical short line for a 268 // long logical row. Getting rid of that requires fixing in 269 // printRow. 270 // 271 // Note how the inter-column spacing is also irrelevant for 272 // that last column. 273 274 if col < last { 275 // (**) See also 'calculateMaxSize' (cMS). Here and 276 // there we have to apply identical transformations to 277 // the cell value to get matching cell width 278 // information. If they do not match then we may here 279 // compute a cell width larger than the max width 280 // found by cMS, derive a negative padding length from 281 // that, and subsequently return an error. What was 282 // further missing is trimming before entering the 283 // user-transform. Especially with color transforms 284 // any trailing space going in will not be removable 285 // for print. 286 // 287 // This happened for 288 // https://www.pivotaltracker.com/n/projects/892938/stories/117404629 289 290 decolorizedLength, err := visibleSize(trim(Decolorize(value))) 291 if err != nil { 292 return err 293 } 294 padlen := t.columnWidth[col] - decolorizedLength 295 padding := strings.Repeat(" ", padlen) 296 fmt.Fprint(result, padding) 297 fmt.Fprint(result, t.colSpacing) 298 } 299 return nil 300 } 301 302 // rowTransformer is an interface behind which we can specify how to 303 // transform the strings of an entire row on a column-by-column basis. 304 type rowTransformer interface { 305 Transform(column int, s string) string 306 } 307 308 // transformHeader is an implementation of rowTransformer which 309 // highlights all columns as a Header. 310 type transformHeader struct{} 311 312 // transHeader holds a package-global transformHeader to prevent us 313 // from continuously allocating a literal of the type whenever we 314 // print a header row. Instead all tables use this value. 315 var transHeader = &transformHeader{} 316 317 // Transform performs the Header highlighting for transformHeader 318 func (th *transformHeader) Transform(column int, s string) string { 319 return HeaderColor(s) 320 } 321 322 // Transform makes a PrintableTable an implementation of 323 // rowTransformer. It performs the per-column transformation for table 324 // content, as specified during construction and/or overridden by the 325 // user of the table, see SetTransformer. 326 func (t *Table) Transform(column int, s string) string { 327 return t.transformer[column](s) 328 } 329 330 // nop is the identity transformation which does not transform the 331 // string at all. 332 func nop(s string) string { 333 return s 334 } 335 336 // trim is a helper to remove trailing whitespace from a string. 337 func trim(s string) string { 338 return strings.TrimRight(s, " \t") 339 } 340 341 // visibleSize returns the number of columns the string will cover 342 // when displayed in the terminal. This is the number of runes, 343 // i.e. characters, not the number of bytes it consists of. 344 func visibleSize(s string) (int, error) { 345 // This code re-implements the basic functionality of 346 // RuneCountInString to account for special cases. Namely 347 // UTF-8 characters taking up 3 bytes (**) appear as double-width. 348 // 349 // (**) I wonder if that is the set of characters outside of 350 // the BMP <=> the set of characters requiring surrogates (2 351 // slots) when encoded in UCS-2. 352 353 r := strings.NewReader(s) 354 355 var size int 356 for range s { 357 _, runeSize, err := r.ReadRune() 358 if err != nil { 359 return -1, fmt.Errorf("error when calculating visible size of: %s", s) 360 } 361 362 if runeSize == 3 { 363 size += 2 // Kanji and Katakana characters appear as double-width 364 } else { 365 size++ 366 } 367 } 368 369 return size, nil 370 }