From ed15bebb841b0fb9fc19962839a701325a977774 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Thu, 11 Mar 2021 18:27:35 +0100 Subject: [PATCH] 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. --- pkg/cmd/gist/list/list_test.go | 17 ++++- utils/table_printer.go | 122 +++++++++++++++++++++++++-------- 2 files changed, 107 insertions(+), 32 deletions(-) diff --git a/pkg/cmd/gist/list/list_test.go b/pkg/cmd/gist/list/list_test.go index 43787916f..e54ddf237 100644 --- a/pkg/cmd/gist/list/list_test.go +++ b/pkg/cmd/gist/list/list_test.go @@ -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", diff --git a/utils/table_printer.go b/utils/table_printer.go index 3047e2d81..2273a201d 100644 --- a/utils/table_printer.go +++ b/utils/table_printer.go @@ -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