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