github.com/andrewhsu/cli/v2@v2.0.1-0.20210910131313-d4b4061f5b89/utils/table_printer.go (about)

     1  package utils
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"sort"
     7  	"strings"
     8  
     9  	"github.com/andrewhsu/cli/v2/pkg/iostreams"
    10  	"github.com/andrewhsu/cli/v2/pkg/text"
    11  )
    12  
    13  type TablePrinter interface {
    14  	IsTTY() bool
    15  	AddField(string, func(int, string) string, func(string) string)
    16  	EndRow()
    17  	Render() error
    18  }
    19  
    20  type TablePrinterOptions struct {
    21  	IsTTY bool
    22  }
    23  
    24  func NewTablePrinter(io *iostreams.IOStreams) TablePrinter {
    25  	return NewTablePrinterWithOptions(io, TablePrinterOptions{
    26  		IsTTY: io.IsStdoutTTY(),
    27  	})
    28  }
    29  
    30  func NewTablePrinterWithOptions(io *iostreams.IOStreams, opts TablePrinterOptions) TablePrinter {
    31  	if opts.IsTTY {
    32  		var maxWidth int
    33  		if io.IsStdoutTTY() {
    34  			maxWidth = io.TerminalWidth()
    35  		} else {
    36  			maxWidth = io.ProcessTerminalWidth()
    37  		}
    38  		return &ttyTablePrinter{
    39  			out:      io.Out,
    40  			maxWidth: maxWidth,
    41  		}
    42  	}
    43  	return &tsvTablePrinter{
    44  		out: io.Out,
    45  	}
    46  }
    47  
    48  type tableField struct {
    49  	Text         string
    50  	TruncateFunc func(int, string) string
    51  	ColorFunc    func(string) string
    52  }
    53  
    54  func (f *tableField) DisplayWidth() int {
    55  	return text.DisplayWidth(f.Text)
    56  }
    57  
    58  type ttyTablePrinter struct {
    59  	out      io.Writer
    60  	maxWidth int
    61  	rows     [][]tableField
    62  }
    63  
    64  func (t ttyTablePrinter) IsTTY() bool {
    65  	return true
    66  }
    67  
    68  func (t *ttyTablePrinter) AddField(s string, truncateFunc func(int, string) string, colorFunc func(string) string) {
    69  	if truncateFunc == nil {
    70  		truncateFunc = text.Truncate
    71  	}
    72  	if t.rows == nil {
    73  		t.rows = make([][]tableField, 1)
    74  	}
    75  	rowI := len(t.rows) - 1
    76  	field := tableField{
    77  		Text:         s,
    78  		TruncateFunc: truncateFunc,
    79  		ColorFunc:    colorFunc,
    80  	}
    81  	t.rows[rowI] = append(t.rows[rowI], field)
    82  }
    83  
    84  func (t *ttyTablePrinter) EndRow() {
    85  	t.rows = append(t.rows, []tableField{})
    86  }
    87  
    88  func (t *ttyTablePrinter) Render() error {
    89  	if len(t.rows) == 0 {
    90  		return nil
    91  	}
    92  
    93  	delim := "  "
    94  	numCols := len(t.rows[0])
    95  	colWidths := t.calculateColumnWidths(len(delim))
    96  
    97  	for _, row := range t.rows {
    98  		for col, field := range row {
    99  			if col > 0 {
   100  				_, err := fmt.Fprint(t.out, delim)
   101  				if err != nil {
   102  					return err
   103  				}
   104  			}
   105  			truncVal := field.TruncateFunc(colWidths[col], field.Text)
   106  			if col < numCols-1 {
   107  				// pad value with spaces on the right
   108  				if padWidth := colWidths[col] - field.DisplayWidth(); padWidth > 0 {
   109  					truncVal += strings.Repeat(" ", padWidth)
   110  				}
   111  			}
   112  			if field.ColorFunc != nil {
   113  				truncVal = field.ColorFunc(truncVal)
   114  			}
   115  			_, err := fmt.Fprint(t.out, truncVal)
   116  			if err != nil {
   117  				return err
   118  			}
   119  		}
   120  		if len(row) > 0 {
   121  			_, err := fmt.Fprint(t.out, "\n")
   122  			if err != nil {
   123  				return err
   124  			}
   125  		}
   126  	}
   127  	return nil
   128  }
   129  
   130  func (t *ttyTablePrinter) calculateColumnWidths(delimSize int) []int {
   131  	numCols := len(t.rows[0])
   132  	allColWidths := make([][]int, numCols)
   133  	for _, row := range t.rows {
   134  		for col, field := range row {
   135  			allColWidths[col] = append(allColWidths[col], field.DisplayWidth())
   136  		}
   137  	}
   138  
   139  	// calculate max & median content width per column
   140  	maxColWidths := make([]int, numCols)
   141  	// medianColWidth := make([]int, numCols)
   142  	for col := 0; col < numCols; col++ {
   143  		widths := allColWidths[col]
   144  		sort.Ints(widths)
   145  		maxColWidths[col] = widths[len(widths)-1]
   146  		// medianColWidth[col] = widths[(len(widths)+1)/2]
   147  	}
   148  
   149  	colWidths := make([]int, numCols)
   150  	// never truncate the first column
   151  	colWidths[0] = maxColWidths[0]
   152  	// never truncate the last column if it contains URLs
   153  	if strings.HasPrefix(t.rows[0][numCols-1].Text, "https://") {
   154  		colWidths[numCols-1] = maxColWidths[numCols-1]
   155  	}
   156  
   157  	availWidth := func() int {
   158  		setWidths := 0
   159  		for col := 0; col < numCols; col++ {
   160  			setWidths += colWidths[col]
   161  		}
   162  		return t.maxWidth - delimSize*(numCols-1) - setWidths
   163  	}
   164  	numFixedCols := func() int {
   165  		fixedCols := 0
   166  		for col := 0; col < numCols; col++ {
   167  			if colWidths[col] > 0 {
   168  				fixedCols++
   169  			}
   170  		}
   171  		return fixedCols
   172  	}
   173  
   174  	// set the widths of short columns
   175  	if w := availWidth(); w > 0 {
   176  		if numFlexColumns := numCols - numFixedCols(); numFlexColumns > 0 {
   177  			perColumn := w / numFlexColumns
   178  			for col := 0; col < numCols; col++ {
   179  				if max := maxColWidths[col]; max < perColumn {
   180  					colWidths[col] = max
   181  				}
   182  			}
   183  		}
   184  	}
   185  
   186  	firstFlexCol := -1
   187  	// truncate long columns to the remaining available width
   188  	if numFlexColumns := numCols - numFixedCols(); numFlexColumns > 0 {
   189  		perColumn := availWidth() / numFlexColumns
   190  		for col := 0; col < numCols; col++ {
   191  			if colWidths[col] == 0 {
   192  				if firstFlexCol == -1 {
   193  					firstFlexCol = col
   194  				}
   195  				if max := maxColWidths[col]; max < perColumn {
   196  					colWidths[col] = max
   197  				} else {
   198  					colWidths[col] = perColumn
   199  				}
   200  			}
   201  		}
   202  	}
   203  
   204  	// add remainder to the first flex column
   205  	if w := availWidth(); w > 0 && firstFlexCol > -1 {
   206  		colWidths[firstFlexCol] += w
   207  		if max := maxColWidths[firstFlexCol]; max < colWidths[firstFlexCol] {
   208  			colWidths[firstFlexCol] = max
   209  		}
   210  	}
   211  
   212  	return colWidths
   213  }
   214  
   215  type tsvTablePrinter struct {
   216  	out        io.Writer
   217  	currentCol int
   218  }
   219  
   220  func (t tsvTablePrinter) IsTTY() bool {
   221  	return false
   222  }
   223  
   224  func (t *tsvTablePrinter) AddField(text string, _ func(int, string) string, _ func(string) string) {
   225  	if t.currentCol > 0 {
   226  		fmt.Fprint(t.out, "\t")
   227  	}
   228  	fmt.Fprint(t.out, text)
   229  	t.currentCol++
   230  }
   231  
   232  func (t *tsvTablePrinter) EndRow() {
   233  	fmt.Fprint(t.out, "\n")
   234  	t.currentCol = 0
   235  }
   236  
   237  func (t *tsvTablePrinter) Render() error {
   238  	return nil
   239  }