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  }