Ensure that table printer fills the full width of the terminal

Sometimes, due to rounding errors, after calculating the width of each
column in a table, the sum of all columns would be shorter that the
total available width in the terminal. This reimplements the elastic
column resizing algorithm to ensure that all available space has been
filled.

As a bonus fix, columns that contain URLs are never truncated.
This commit is contained in:
Mislav Marohnić 2021-03-11 18:27:35 +01:00
parent eddd8f00d1
commit ed15bebb84
2 changed files with 107 additions and 32 deletions

View file

@ -177,7 +177,12 @@ func Test_listRun(t *testing.T) {
)),
)
},
wantOut: "1234567890 cool.txt 1 file public about 6 hours ago\n4567890123 1 file public about 6 hours ago\n2345678901 tea leaves thwart... 2 files secret about 6 hours ago\n3456789012 short desc 11 files secret about 6 hours ago\n",
wantOut: heredoc.Doc(`
1234567890 cool.txt 1 file public about 6 hours ago
4567890123 1 file public about 6 hours ago
2345678901 tea leaves thwart those who ... 2 files secret about 6 hours ago
3456789012 short desc 11 files secret about 6 hours ago
`),
},
{
name: "with public filter",
@ -206,7 +211,10 @@ func Test_listRun(t *testing.T) {
)),
)
},
wantOut: "1234567890 cool.txt 1 file public about 6 hours ago\n4567890123 1 file public about 6 hours ago\n",
wantOut: heredoc.Doc(`
1234567890 cool.txt 1 file public about 6 hours ago
4567890123 1 file public about 6 hours ago
`),
},
{
name: "with secret filter",
@ -250,7 +258,10 @@ func Test_listRun(t *testing.T) {
)),
)
},
wantOut: "2345678901 tea leaves thwart... 2 files secret about 6 hours ago\n3456789012 short desc 11 files secret about 6 hours ago\n",
wantOut: heredoc.Doc(`
2345678901 tea leaves thwart those who ... 2 files secret about 6 hours ago
3456789012 short desc 11 files secret about 6 hours ago
`),
},
{
name: "with limit",

View file

@ -3,6 +3,7 @@ package utils
import (
"fmt"
"io"
"sort"
"strings"
"github.com/cli/cli/pkg/iostreams"
@ -34,6 +35,10 @@ type tableField struct {
ColorFunc func(string) string
}
func (f *tableField) DisplayWidth() int {
return text.DisplayWidth(f.Text)
}
type ttyTablePrinter struct {
out io.Writer
maxWidth int
@ -69,35 +74,9 @@ func (t *ttyTablePrinter) Render() error {
return nil
}
numCols := len(t.rows[0])
colWidths := make([]int, numCols)
// measure maximum content width per column
for _, row := range t.rows {
for col, field := range row {
textLen := text.DisplayWidth(field.Text)
if textLen > colWidths[col] {
colWidths[col] = textLen
}
}
}
delim := " "
availWidth := t.maxWidth - colWidths[0] - ((numCols - 1) * len(delim))
// add extra space from columns that are already narrower than threshold
for col := 1; col < numCols; col++ {
availColWidth := availWidth / (numCols - 1)
if extra := availColWidth - colWidths[col]; extra > 0 {
availWidth += extra
}
}
// cap all but first column to fit available terminal width
// TODO: support weighted instead of even redistribution
for col := 1; col < numCols; col++ {
availColWidth := availWidth / (numCols - 1)
if colWidths[col] > availColWidth {
colWidths[col] = availColWidth
}
}
numCols := len(t.rows[0])
colWidths := t.calculateColumnWidths(len(delim))
for _, row := range t.rows {
for col, field := range row {
@ -110,7 +89,7 @@ func (t *ttyTablePrinter) Render() error {
truncVal := field.TruncateFunc(colWidths[col], field.Text)
if col < numCols-1 {
// pad value with spaces on the right
if padWidth := colWidths[col] - text.DisplayWidth(field.Text); padWidth > 0 {
if padWidth := colWidths[col] - field.DisplayWidth(); padWidth > 0 {
truncVal += strings.Repeat(" ", padWidth)
}
}
@ -132,6 +111,91 @@ func (t *ttyTablePrinter) Render() error {
return nil
}
func (t *ttyTablePrinter) calculateColumnWidths(delimSize int) []int {
numCols := len(t.rows[0])
allColWidths := make([][]int, numCols)
for _, row := range t.rows {
for col, field := range row {
allColWidths[col] = append(allColWidths[col], field.DisplayWidth())
}
}
// calculate max & median content width per column
maxColWidths := make([]int, numCols)
// medianColWidth := make([]int, numCols)
for col := 0; col < numCols; col++ {
widths := allColWidths[col]
sort.Ints(widths)
maxColWidths[col] = widths[len(widths)-1]
// medianColWidth[col] = widths[(len(widths)+1)/2]
}
colWidths := make([]int, numCols)
// never truncate the first column
colWidths[0] = maxColWidths[0]
// never truncate the last column if it contains URLs
if strings.HasPrefix(t.rows[0][numCols-1].Text, "https://") {
colWidths[numCols-1] = maxColWidths[numCols-1]
}
availWidth := func() int {
setWidths := 0
for col := 0; col < numCols; col++ {
setWidths += colWidths[col]
}
return t.maxWidth - delimSize*(numCols-1) - setWidths
}
numFixedCols := func() int {
fixedCols := 0
for col := 0; col < numCols; col++ {
if colWidths[col] > 0 {
fixedCols++
}
}
return fixedCols
}
// set the widths of short columns
if w := availWidth(); w > 0 {
if numFlexColumns := numCols - numFixedCols(); numFlexColumns > 0 {
perColumn := w / numFlexColumns
for col := 0; col < numCols; col++ {
if max := maxColWidths[col]; max < perColumn {
colWidths[col] = max
}
}
}
}
firstFlexCol := -1
// truncate long columns to the remaining available width
if numFlexColumns := numCols - numFixedCols(); numFlexColumns > 0 {
perColumn := availWidth() / numFlexColumns
for col := 0; col < numCols; col++ {
if colWidths[col] == 0 {
if firstFlexCol == -1 {
firstFlexCol = col
}
if max := maxColWidths[col]; max < perColumn {
colWidths[col] = max
} else {
colWidths[col] = perColumn
}
}
}
}
// add remainder to the first flex column
if w := availWidth(); w > 0 && firstFlexCol > -1 {
colWidths[firstFlexCol] += w
if max := maxColWidths[firstFlexCol]; max < colWidths[firstFlexCol] {
colWidths[firstFlexCol] = max
}
}
return colWidths
}
type tsvTablePrinter struct {
out io.Writer
currentCol int