github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/flexibletable/table.go (about)

     1  // Copyright 2016 Keybase, Inc. All rights reserved. Use of
     2  // this source code is governed by the included BSD license.
     3  
     4  package flexibletable
     5  
     6  import (
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"strings"
    11  )
    12  
    13  // ColumnConstraint specifies how a column should behave while being rendered.
    14  // Use positive to specify a maximum width for the column, or one of const
    15  // values for expandable width.
    16  type ColumnConstraint int
    17  
    18  const (
    19  
    20  	// Expandable is a special ColumnConstraint where the column and may expand
    21  	// automatically if other columns end up taking less actual width.
    22  	Expandable ColumnConstraint = 0
    23  
    24  	// ExpandableWrappable is a special ColumnConstraint where the column is
    25  	// expandable. In addition, it  can wrap into multiple lines if needed.
    26  	ExpandableWrappable ColumnConstraint = -1
    27  )
    28  
    29  // Row defines a row
    30  type Row []Cell
    31  
    32  // Table defines a table and is used to do the rendering
    33  type Table struct {
    34  	rows     []Row
    35  	nInserts int
    36  }
    37  
    38  // Insert inserts a row into the table
    39  func (t *Table) Insert(row Row) error {
    40  	if len(t.rows) > 0 && len(t.rows[0]) != len(row) {
    41  		return InconsistentRowsError{existingRows: len(t.rows), newRow: len(row)}
    42  	}
    43  	t.rows = append(t.rows, row)
    44  	t.nInserts++
    45  	return nil
    46  }
    47  
    48  func (t *Table) NumInserts() int {
    49  	return t.nInserts
    50  }
    51  
    52  func (t *Table) breakOnLineBreaks() error {
    53  
    54  	// so that there's no need to resize if there's no line break
    55  	broken := make([]Row, 0, len(t.rows))
    56  
    57  	for _, row := range t.rows {
    58  
    59  		notEmpty := true
    60  		for notEmpty {
    61  			newRow := make(Row, 0, len(row))
    62  			notEmpty = false
    63  
    64  			for iCell := range row {
    65  				switch content := row[iCell].Content.(type) {
    66  				case emptyCell:
    67  					newRow = append(newRow, Cell{
    68  						Alignment: row[iCell].Alignment,
    69  						Frame:     [2]string{"", ""},
    70  						Content:   row[iCell].Content,
    71  					})
    72  				case MultiCell:
    73  					notEmpty = true
    74  					for iItem := range content.Items {
    75  						// we are replacing line breaks with spaces for MultiCell for now
    76  						content.Items[iItem] = strings.ReplaceAll(content.Items[iItem], "\n", " ")
    77  					}
    78  					newRow = append(newRow, Cell{
    79  						Alignment: row[iCell].Alignment,
    80  						Frame:     row[iCell].Frame,
    81  						Content:   content,
    82  					})
    83  					row[iCell].Content = emptyCell{}
    84  				case SingleCell:
    85  					notEmpty = true
    86  					lb := strings.Index(content.Item, "\n")
    87  					current := ""
    88  					if lb >= 0 {
    89  						current = content.Item[:lb]
    90  						row[iCell].Content = SingleCell{Item: content.Item[lb+1:]}
    91  					} else {
    92  						current = content.Item
    93  						row[iCell].Content = emptyCell{}
    94  					}
    95  					newRow = append(newRow, Cell{
    96  						Alignment: row[iCell].Alignment,
    97  						Frame:     row[iCell].Frame,
    98  						Content:   SingleCell{Item: current},
    99  					})
   100  				default:
   101  					// unexported error because this shouldn't happen unless we make a
   102  					// mistake in code
   103  					return errors.New("unexpected cell content")
   104  				}
   105  			}
   106  
   107  			if notEmpty {
   108  				broken = append(broken, newRow)
   109  			}
   110  		}
   111  
   112  	}
   113  
   114  	t.rows = broken
   115  	return nil
   116  }
   117  
   118  func (t Table) renderFirstPass(cellSep string, maxWidth int, constraints []ColumnConstraint) (widths []int, err error) {
   119  	numOfNoConstraints := 0
   120  	for _, c := range constraints {
   121  		if c <= 0 {
   122  			numOfNoConstraints++
   123  		}
   124  	}
   125  	if numOfNoConstraints == 0 {
   126  		numOfNoConstraints = 1
   127  	}
   128  
   129  	// first pass; determine smallest width for each column under constraints
   130  	widths = make([]int, len(t.rows[0]))
   131  	for _, row := range t.rows {
   132  		for i, c := range row {
   133  			if constraints[i] > 0 {
   134  				str, err := c.render(int(constraints[i]))
   135  				if err != nil {
   136  					return nil, err
   137  				}
   138  				if widths[i] < len(str) {
   139  					widths[i] = len(str)
   140  				}
   141  			}
   142  		}
   143  	}
   144  
   145  	// calculate width for un-constrained columns
   146  	rest := maxWidth - len(cellSep)*(len(widths)-1) // take out cellSeps
   147  	for _, w := range widths {
   148  		rest -= w
   149  	}
   150  	each := rest / numOfNoConstraints
   151  	last := -1
   152  	for i := range widths {
   153  		if constraints[i] <= 0 {
   154  			widths[i] = each
   155  			last = i
   156  		}
   157  	}
   158  	if last != -1 {
   159  		widths[last] = rest - each*(numOfNoConstraints-1)
   160  	}
   161  
   162  	return widths, nil
   163  }
   164  
   165  func (t Table) renderSecondPass(constraints []ColumnConstraint, widths []int) (rows [][]string, err error) {
   166  	// actually rendering
   167  
   168  	for _, row := range t.rows {
   169  		var strs []string
   170  		for ic, c := range row {
   171  			if constraints[ic] >= 0 {
   172  				str, err := c.renderWithPadding(widths[ic])
   173  				if err != nil {
   174  					return nil, err
   175  				}
   176  				strs = append(strs, str)
   177  			} else { // need wrapping!
   178  				strs = append(strs, c.full())
   179  			}
   180  		}
   181  
   182  		wrapping := true
   183  		for wrapping {
   184  			var toAppend []string
   185  			wrapping = false
   186  			for i := range strs {
   187  				if widths[i] < len(strs[i]) {
   188  					toAppend = append(toAppend, strs[i][:widths[i]])
   189  					strs[i] = strs[i][widths[i]:]
   190  					wrapping = true
   191  				} else {
   192  					str, err := row[i].addPadding(strs[i], widths[i])
   193  					if err != nil {
   194  						return nil, err
   195  					}
   196  					toAppend = append(toAppend, str)
   197  					strs[i] = strings.Repeat(" ", widths[i])
   198  				}
   199  			}
   200  			rows = append(rows, toAppend)
   201  		}
   202  	}
   203  
   204  	return rows, nil
   205  }
   206  
   207  // Render renders the table into writer. The constraints parameter specifies
   208  // how each column should be constrained while being rendered. Positive values
   209  // limit the maximum width.
   210  func (t Table) Render(w io.Writer, cellSep string, maxWidth int, constraints []ColumnConstraint) error {
   211  	if len(t.rows) == 0 {
   212  		return NoRowsError{}
   213  	}
   214  	if len(constraints) != len(t.rows[0]) {
   215  		return InconsistentRowsError{existingRows: len(t.rows[0]), newRow: len(constraints)}
   216  	}
   217  
   218  	err := t.breakOnLineBreaks()
   219  	if err != nil {
   220  		return err
   221  	}
   222  
   223  	widths, err := t.renderFirstPass(cellSep, maxWidth, constraints)
   224  	if err != nil {
   225  		return err
   226  	}
   227  
   228  	rows, err := t.renderSecondPass(constraints, widths)
   229  	if err != nil {
   230  		return err
   231  	}
   232  
   233  	// write out
   234  	for _, row := range rows {
   235  		fmt.Fprint(w, strings.Join(row, cellSep))
   236  		fmt.Fprintln(w)
   237  	}
   238  
   239  	return nil
   240  }