diff options
Diffstat (limited to 'internal/ui/table.go')
| -rw-r--r-- | internal/ui/table.go | 107 |
1 files changed, 89 insertions, 18 deletions
diff --git a/internal/ui/table.go b/internal/ui/table.go index 5ef2c99..e85e505 100644 --- a/internal/ui/table.go +++ b/internal/ui/table.go @@ -38,6 +38,68 @@ type Table struct { Rows [][]string } +// rebalanceWidths shrinks column widths in place so their sum fits budget. +// Rather than crushing only the last column, every column wider than the +// fair share (budget/nCols) is capped first, then columns are trimmed one +// rune at a time from the widest until the table fits. Leftover slack is +// handed back round-robin to columns that lost width so wide layouts still +// use the full line. +func rebalanceWidths(colWidths []int, budget int) { + original := make([]int, len(colWidths)) + copy(original, colWidths) + + fair := budget / len(colWidths) + if fair < 1 { + fair = 1 + } + for i, w := range colWidths { + if w > fair { + colWidths[i] = fair + } + } + + sum := func() int { + t := 0 + for _, w := range colWidths { + t += w + } + return t + } + + // Tight tables: trim the widest column until we fit (or hit 1). + for sum() > budget { + widest := 0 + for i, w := range colWidths { + if w > colWidths[widest] { + widest = i + } + } + if colWidths[widest] <= 1 { + break + } + colWidths[widest]-- + } + + // Give unused width back to the columns that were reduced. + for { + extra := budget - sum() + if extra <= 0 { + break + } + progress := false + for i := 0; i < len(colWidths) && extra > 0; i++ { + if colWidths[i] < original[i] { + colWidths[i]++ + extra-- + progress = true + } + } + if !progress { + break + } + } +} + func (t *Table) Render(unicode bool, maxWidth int) []string { g := tableGlyphSet(unicode) @@ -65,29 +127,38 @@ func (t *Table) Render(unicode bool, maxWidth int) []string { } } + headerCells := t.Columns + bodyRows := t.Rows + if maxWidth > 0 { + // Cell padding: one space on each side of every cell, plus one + // separator between columns. overhead := 3*nCols + 1 - total := overhead + budget := maxWidth - overhead + if budget < nCols { + budget = nCols + } + + total := 0 for _, w := range colWidths { total += w } - if total > maxWidth { - last := nCols - 1 - budget := maxWidth - overhead - for i := 0; i < last; i++ { - budget -= colWidths[i] - } - if budget < 5 { - budget = 5 - } - colWidths[last] = budget - - if len(t.Columns) > last { - t.Columns[last] = color.TruncateVisible(t.Columns[last], budget, unicode) + if total > budget { + rebalanceWidths(colWidths, budget) + + // Truncate every cell to its (possibly reduced) column width. + // Work on copies so rendering never mutates the table. + headerCells = make([]string, len(t.Columns)) + copy(headerCells, t.Columns) + for i := range headerCells { + headerCells[i] = color.TruncateVisible(headerCells[i], colWidths[i], unicode) } + bodyRows = make([][]string, len(t.Rows)) for i := range t.Rows { - if len(t.Rows[i]) > last { - t.Rows[i][last] = color.TruncateVisible(t.Rows[i][last], budget, unicode) + bodyRows[i] = make([]string, len(t.Rows[i])) + copy(bodyRows[i], t.Rows[i]) + for j := 0; j < len(bodyRows[i]) && j < nCols; j++ { + bodyRows[i][j] = color.TruncateVisible(bodyRows[i][j], colWidths[j], unicode) } } } @@ -164,11 +235,11 @@ func (t *Table) Render(unicode bool, maxWidth int) []string { } if hasCols { - out = append(out, makeRow(t.Columns)) + out = append(out, makeRow(headerCells)) out = append(out, makeSep(g.sepLeft, g.sepCross, g.sepRight, g.fillHSep)) } - for _, row := range t.Rows { + for _, row := range bodyRows { out = append(out, makeRow(row)) } |
