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 }