Skip to content

Commit 7862f52

Browse files
committed
feat(table): improve sizing and behavior: wrap by default, overflow optionally
1 parent 0fbb070 commit 7862f52

14 files changed

+1031
-174
lines changed

table/resizing.go

Lines changed: 401 additions & 0 deletions
Large diffs are not rendered by default.

table/rows.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,19 @@ func (m *Filter) Rows() int {
111111

112112
return j
113113
}
114+
115+
// dataToMatrix converts an object that implements the Data interface to a table.
116+
func dataToMatrix(data Data) (rows [][]string) {
117+
numRows := data.Rows()
118+
numCols := data.Columns()
119+
rows = make([][]string, numRows)
120+
121+
for i := 0; i < numRows; i++ {
122+
rows[i] = make([]string, numCols)
123+
124+
for j := 0; j < numCols; j++ {
125+
rows[i][j] = data.At(i, j)
126+
}
127+
}
128+
return
129+
}

table/table.go

Lines changed: 15 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ type Table struct {
6161
height int
6262
useManualHeight bool
6363
offset int
64+
wrap bool
6465

6566
// widths tracks the width of each column.
6667
widths []int
@@ -83,6 +84,7 @@ func New() *Table {
8384
borderLeft: true,
8485
borderRight: true,
8586
borderTop: true,
87+
wrap: true,
8688
data: NewStringData(),
8789
}
8890
}
@@ -217,6 +219,12 @@ func (t *Table) Offset(o int) *Table {
217219
return t
218220
}
219221

222+
// Wrap dictates whether or not the table content should wrap.
223+
func (t *Table) Wrap(w bool) *Table {
224+
t.wrap = w
225+
return t
226+
}
227+
220228
// String returns the table as a string.
221229
func (t *Table) String() string {
222230
hasHeaders := len(t.headers) > 0
@@ -234,120 +242,8 @@ func (t *Table) String() string {
234242
}
235243
}
236244

237-
// Initialize the widths.
238-
t.widths = make([]int, max(len(t.headers), t.data.Columns()))
239-
t.heights = make([]int, btoi(hasHeaders)+t.data.Rows())
240-
241-
// The style function may affect width of the table. It's possible to set
242-
// the StyleFunc after the headers and rows. Update the widths for a final
243-
// time.
244-
for i, cell := range t.headers {
245-
t.widths[i] = max(t.widths[i], lipgloss.Width(t.style(HeaderRow, i).Render(cell)))
246-
t.heights[0] = max(t.heights[0], lipgloss.Height(t.style(HeaderRow, i).Render(cell)))
247-
}
248-
249-
for r := 0; r < t.data.Rows(); r++ {
250-
for i := 0; i < t.data.Columns(); i++ {
251-
cell := t.data.At(r, i)
252-
253-
rendered := t.style(r, i).Render(cell)
254-
t.heights[r+btoi(hasHeaders)] = max(t.heights[r+btoi(hasHeaders)], lipgloss.Height(rendered))
255-
t.widths[i] = max(t.widths[i], lipgloss.Width(rendered))
256-
}
257-
}
258-
259-
// Table Resizing Logic.
260-
//
261-
// Given a user defined table width, we must ensure the table is exactly that
262-
// width. This must account for all borders, column, separators, and column
263-
// data.
264-
//
265-
// In the case where the table is narrower than the specified table width,
266-
// we simply expand the columns evenly to fit the width.
267-
// For example, a table with 3 columns takes up 50 characters total, and the
268-
// width specified is 80, we expand each column by 10 characters, adding 30
269-
// to the total width.
270-
//
271-
// In the case where the table is wider than the specified table width, we
272-
// _could_ simply shrink the columns evenly but this would result in data
273-
// being truncated (perhaps unnecessarily). The naive approach could result
274-
// in very poor cropping of the table data. So, instead of shrinking columns
275-
// evenly, we calculate the median non-whitespace length of each column, and
276-
// shrink the columns based on the largest median.
277-
//
278-
// For example,
279-
// ┌──────┬───────────────┬──────────┐
280-
// │ Name │ Age of Person │ Location │
281-
// ├──────┼───────────────┼──────────┤
282-
// │ Kini │ 40 │ New York │
283-
// │ Eli │ 30 │ London │
284-
// │ Iris │ 20 │ Paris │
285-
// └──────┴───────────────┴──────────┘
286-
//
287-
// Median non-whitespace length vs column width of each column:
288-
//
289-
// Name: 4 / 5
290-
// Age of Person: 2 / 15
291-
// Location: 6 / 10
292-
//
293-
// The biggest difference is 15 - 2, so we can shrink the 2nd column by 13.
294-
295-
width := t.computeWidth()
296-
297-
if width < t.width && t.width > 0 {
298-
// Table is too narrow, expand the columns evenly until it reaches the
299-
// desired width.
300-
var i int
301-
for width < t.width {
302-
t.widths[i]++
303-
width++
304-
i = (i + 1) % len(t.widths)
305-
}
306-
} else if width > t.width && t.width > 0 {
307-
// Table is too wide, calculate the median non-whitespace length of each
308-
// column, and shrink the columns based on the largest difference.
309-
columnMedians := make([]int, len(t.widths))
310-
for c := range t.widths {
311-
trimmedWidth := make([]int, t.data.Rows())
312-
for r := 0; r < t.data.Rows(); r++ {
313-
renderedCell := t.style(r+btoi(hasHeaders), c).Render(t.data.At(r, c))
314-
nonWhitespaceChars := lipgloss.Width(strings.TrimRight(renderedCell, " "))
315-
trimmedWidth[r] = nonWhitespaceChars + 1
316-
}
317-
318-
columnMedians[c] = median(trimmedWidth)
319-
}
320-
321-
// Find the biggest differences between the median and the column width.
322-
// Shrink the columns based on the largest difference.
323-
differences := make([]int, len(t.widths))
324-
for i := range t.widths {
325-
differences[i] = t.widths[i] - columnMedians[i]
326-
}
327-
328-
for width > t.width {
329-
index, _ := largest(differences)
330-
if differences[index] < 1 {
331-
break
332-
}
333-
334-
shrink := min(differences[index], width-t.width)
335-
t.widths[index] -= shrink
336-
width -= shrink
337-
differences[index] = 0
338-
}
339-
340-
// Table is still too wide, begin shrinking the columns based on the
341-
// largest column.
342-
for width > t.width {
343-
index, _ := largest(t.widths)
344-
if t.widths[index] < 1 {
345-
break
346-
}
347-
t.widths[index]--
348-
width--
349-
}
350-
}
245+
// Do all the sizing calculations for width and height.
246+
t.resize()
351247

352248
var sb strings.Builder
353249

@@ -396,15 +292,6 @@ func (t *Table) String() string {
396292
Render(sb.String())
397293
}
398294

399-
// computeWidth computes the width of the table in it's current configuration.
400-
func (t *Table) computeWidth() int {
401-
width := sum(t.widths) + btoi(t.borderLeft) + btoi(t.borderRight)
402-
if t.borderColumn {
403-
width += len(t.widths) - 1
404-
}
405-
return width
406-
}
407-
408295
// computeHeight computes the height of the table in it's current configuration.
409296
func (t *Table) computeHeight() int {
410297
hasHeaders := len(t.headers) > 0
@@ -556,13 +443,17 @@ func (t *Table) constructRow(index int, isOverflow bool) string {
556443
}
557444

558445
cellStyle := t.style(index, c)
446+
if !t.wrap {
447+
length := (cellWidth * height) - cellStyle.GetHorizontalPadding()
448+
cell = ansi.Truncate(cell, length, "…")
449+
}
559450
cells = append(cells, cellStyle.
560451
// Account for the margins in the cell sizing.
561452
Height(height-cellStyle.GetVerticalMargins()).
562453
MaxHeight(height).
563454
Width(t.widths[c]-cellStyle.GetHorizontalMargins()).
564455
MaxWidth(t.widths[c]).
565-
Render(ansi.Truncate(cell, cellWidth*height, "…")))
456+
Render(cell))
566457

567458
if c < t.data.Columns()-1 && t.borderColumn {
568459
cells = append(cells, left)

0 commit comments

Comments
 (0)