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.
223 lines
4.8 KiB
Go
223 lines
4.8 KiB
Go
package utils
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/cli/cli/pkg/iostreams"
|
|
"github.com/cli/cli/pkg/text"
|
|
)
|
|
|
|
type TablePrinter interface {
|
|
IsTTY() bool
|
|
AddField(string, func(int, string) string, func(string) string)
|
|
EndRow()
|
|
Render() error
|
|
}
|
|
|
|
func NewTablePrinter(io *iostreams.IOStreams) TablePrinter {
|
|
if io.IsStdoutTTY() {
|
|
return &ttyTablePrinter{
|
|
out: io.Out,
|
|
maxWidth: io.TerminalWidth(),
|
|
}
|
|
}
|
|
return &tsvTablePrinter{
|
|
out: io.Out,
|
|
}
|
|
}
|
|
|
|
type tableField struct {
|
|
Text string
|
|
TruncateFunc func(int, string) string
|
|
ColorFunc func(string) string
|
|
}
|
|
|
|
func (f *tableField) DisplayWidth() int {
|
|
return text.DisplayWidth(f.Text)
|
|
}
|
|
|
|
type ttyTablePrinter struct {
|
|
out io.Writer
|
|
maxWidth int
|
|
rows [][]tableField
|
|
}
|
|
|
|
func (t ttyTablePrinter) IsTTY() bool {
|
|
return true
|
|
}
|
|
|
|
func (t *ttyTablePrinter) AddField(s string, truncateFunc func(int, string) string, colorFunc func(string) string) {
|
|
if truncateFunc == nil {
|
|
truncateFunc = text.Truncate
|
|
}
|
|
if t.rows == nil {
|
|
t.rows = make([][]tableField, 1)
|
|
}
|
|
rowI := len(t.rows) - 1
|
|
field := tableField{
|
|
Text: s,
|
|
TruncateFunc: truncateFunc,
|
|
ColorFunc: colorFunc,
|
|
}
|
|
t.rows[rowI] = append(t.rows[rowI], field)
|
|
}
|
|
|
|
func (t *ttyTablePrinter) EndRow() {
|
|
t.rows = append(t.rows, []tableField{})
|
|
}
|
|
|
|
func (t *ttyTablePrinter) Render() error {
|
|
if len(t.rows) == 0 {
|
|
return nil
|
|
}
|
|
|
|
delim := " "
|
|
numCols := len(t.rows[0])
|
|
colWidths := t.calculateColumnWidths(len(delim))
|
|
|
|
for _, row := range t.rows {
|
|
for col, field := range row {
|
|
if col > 0 {
|
|
_, err := fmt.Fprint(t.out, delim)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
truncVal := field.TruncateFunc(colWidths[col], field.Text)
|
|
if col < numCols-1 {
|
|
// pad value with spaces on the right
|
|
if padWidth := colWidths[col] - field.DisplayWidth(); padWidth > 0 {
|
|
truncVal += strings.Repeat(" ", padWidth)
|
|
}
|
|
}
|
|
if field.ColorFunc != nil {
|
|
truncVal = field.ColorFunc(truncVal)
|
|
}
|
|
_, err := fmt.Fprint(t.out, truncVal)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if len(row) > 0 {
|
|
_, err := fmt.Fprint(t.out, "\n")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
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
|
|
}
|
|
|
|
func (t tsvTablePrinter) IsTTY() bool {
|
|
return false
|
|
}
|
|
|
|
func (t *tsvTablePrinter) AddField(text string, _ func(int, string) string, _ func(string) string) {
|
|
if t.currentCol > 0 {
|
|
fmt.Fprint(t.out, "\t")
|
|
}
|
|
fmt.Fprint(t.out, text)
|
|
t.currentCol++
|
|
}
|
|
|
|
func (t *tsvTablePrinter) EndRow() {
|
|
fmt.Fprint(t.out, "\n")
|
|
t.currentCol = 0
|
|
}
|
|
|
|
func (t *tsvTablePrinter) Render() error {
|
|
return nil
|
|
}
|