...

Source file src/github.com/cli/go-gh/v2/pkg/tableprinter/table.go

Documentation: github.com/cli/go-gh/v2/pkg/tableprinter

     1  // Package tableprinter facilitates rendering column-formatted data to a terminal and TSV-formatted data to
     2  // a script or a file. It is suitable for presenting tabular data in a human-readable format that is
     3  // guaranteed to fit within the given viewport, while at the same time offering the same data in a
     4  // machine-readable format for scripts.
     5  package tableprinter
     6  
     7  import (
     8  	"fmt"
     9  	"io"
    10  
    11  	"github.com/cli/go-gh/v2/pkg/text"
    12  )
    13  
    14  type fieldOption func(*tableField)
    15  
    16  type TablePrinter interface {
    17  	AddHeader([]string, ...fieldOption)
    18  	AddField(string, ...fieldOption)
    19  	EndRow()
    20  	Render() error
    21  }
    22  
    23  // WithTruncate overrides the truncation function for the field. The function should transform a string
    24  // argument into a string that fits within the given display width. The default behavior is to truncate the
    25  // value by adding "..." in the end. The truncation function will be called before padding and coloring.
    26  // Pass nil to disable truncation for this value.
    27  func WithTruncate(fn func(int, string) string) fieldOption {
    28  	return func(f *tableField) {
    29  		f.truncateFunc = fn
    30  	}
    31  }
    32  
    33  // WithPadding overrides the padding function for the field. The function should transform a string argument
    34  // into a string that is padded to fit within the given display width. The default behavior is to pad fields
    35  // with spaces except for the last field. The padding function will be called after truncation and before coloring.
    36  // Pass nil to disable padding for this value.
    37  func WithPadding(fn func(int, string) string) fieldOption {
    38  	return func(f *tableField) {
    39  		f.paddingFunc = fn
    40  	}
    41  }
    42  
    43  // WithColor sets the color function for the field. The function should transform a string value by wrapping
    44  // it in ANSI escape codes. The color function will not be used if the table was initialized in non-terminal mode.
    45  // The color function will be called before truncation and padding.
    46  func WithColor(fn func(string) string) fieldOption {
    47  	return func(f *tableField) {
    48  		f.colorFunc = fn
    49  	}
    50  }
    51  
    52  // New initializes a table printer with terminal mode and terminal width. When terminal mode is enabled, the
    53  // output will be human-readable, column-formatted to fit available width, and rendered with color support.
    54  // In non-terminal mode, the output is tab-separated and all truncation of values is disabled.
    55  func New(w io.Writer, isTTY bool, maxWidth int) TablePrinter {
    56  	if isTTY {
    57  		return &ttyTablePrinter{
    58  			out:      w,
    59  			maxWidth: maxWidth,
    60  		}
    61  	}
    62  
    63  	return &tsvTablePrinter{
    64  		out: w,
    65  	}
    66  }
    67  
    68  type tableField struct {
    69  	text         string
    70  	truncateFunc func(int, string) string
    71  	paddingFunc  func(int, string) string
    72  	colorFunc    func(string) string
    73  }
    74  
    75  type ttyTablePrinter struct {
    76  	out        io.Writer
    77  	maxWidth   int
    78  	hasHeaders bool
    79  	rows       [][]tableField
    80  }
    81  
    82  func (t *ttyTablePrinter) AddHeader(columns []string, opts ...fieldOption) {
    83  	if t.hasHeaders {
    84  		return
    85  	}
    86  
    87  	t.hasHeaders = true
    88  	for _, column := range columns {
    89  		t.AddField(column, opts...)
    90  	}
    91  	t.EndRow()
    92  }
    93  
    94  func (t *ttyTablePrinter) AddField(s string, opts ...fieldOption) {
    95  	if t.rows == nil {
    96  		t.rows = make([][]tableField, 1)
    97  	}
    98  	rowI := len(t.rows) - 1
    99  	field := tableField{
   100  		text:         s,
   101  		truncateFunc: text.Truncate,
   102  	}
   103  	for _, opt := range opts {
   104  		opt(&field)
   105  	}
   106  	t.rows[rowI] = append(t.rows[rowI], field)
   107  }
   108  
   109  func (t *ttyTablePrinter) EndRow() {
   110  	t.rows = append(t.rows, []tableField{})
   111  }
   112  
   113  func (t *ttyTablePrinter) Render() error {
   114  	if len(t.rows) == 0 {
   115  		return nil
   116  	}
   117  
   118  	delim := "  "
   119  	numCols := len(t.rows[0])
   120  	colWidths := t.calculateColumnWidths(len(delim))
   121  
   122  	for _, row := range t.rows {
   123  		for col, field := range row {
   124  			if col > 0 {
   125  				_, err := fmt.Fprint(t.out, delim)
   126  				if err != nil {
   127  					return err
   128  				}
   129  			}
   130  			truncVal := field.text
   131  			if field.truncateFunc != nil {
   132  				truncVal = field.truncateFunc(colWidths[col], field.text)
   133  			}
   134  			if field.paddingFunc != nil {
   135  				truncVal = field.paddingFunc(colWidths[col], truncVal)
   136  			} else if col < numCols-1 {
   137  				truncVal = text.PadRight(colWidths[col], truncVal)
   138  			}
   139  			if field.colorFunc != nil {
   140  				truncVal = field.colorFunc(truncVal)
   141  			}
   142  			_, err := fmt.Fprint(t.out, truncVal)
   143  			if err != nil {
   144  				return err
   145  			}
   146  		}
   147  		if len(row) > 0 {
   148  			_, err := fmt.Fprint(t.out, "\n")
   149  			if err != nil {
   150  				return err
   151  			}
   152  		}
   153  	}
   154  	return nil
   155  }
   156  
   157  func (t *ttyTablePrinter) calculateColumnWidths(delimSize int) []int {
   158  	numCols := len(t.rows[0])
   159  	maxColWidths := make([]int, numCols)
   160  	colWidths := make([]int, numCols)
   161  
   162  	for _, row := range t.rows {
   163  		for col, field := range row {
   164  			w := text.DisplayWidth(field.text)
   165  			if w > maxColWidths[col] {
   166  				maxColWidths[col] = w
   167  			}
   168  			// if this field has disabled truncating, ensure that the column is wide enough
   169  			if field.truncateFunc == nil && w > colWidths[col] {
   170  				colWidths[col] = w
   171  			}
   172  		}
   173  	}
   174  
   175  	availWidth := func() int {
   176  		setWidths := 0
   177  		for col := 0; col < numCols; col++ {
   178  			setWidths += colWidths[col]
   179  		}
   180  		return t.maxWidth - delimSize*(numCols-1) - setWidths
   181  	}
   182  	numFixedCols := func() int {
   183  		fixedCols := 0
   184  		for col := 0; col < numCols; col++ {
   185  			if colWidths[col] > 0 {
   186  				fixedCols++
   187  			}
   188  		}
   189  		return fixedCols
   190  	}
   191  
   192  	// set the widths of short columns
   193  	if w := availWidth(); w > 0 {
   194  		if numFlexColumns := numCols - numFixedCols(); numFlexColumns > 0 {
   195  			perColumn := w / numFlexColumns
   196  			for col := 0; col < numCols; col++ {
   197  				if max := maxColWidths[col]; max < perColumn {
   198  					colWidths[col] = max
   199  				}
   200  			}
   201  		}
   202  	}
   203  
   204  	// truncate long columns to the remaining available width
   205  	if numFlexColumns := numCols - numFixedCols(); numFlexColumns > 0 {
   206  		perColumn := availWidth() / numFlexColumns
   207  		for col := 0; col < numCols; col++ {
   208  			if colWidths[col] == 0 {
   209  				if max := maxColWidths[col]; max < perColumn {
   210  					colWidths[col] = max
   211  				} else if perColumn > 0 {
   212  					colWidths[col] = perColumn
   213  				}
   214  			}
   215  		}
   216  	}
   217  
   218  	// add the remainder to truncated columns
   219  	if w := availWidth(); w > 0 {
   220  		for col := 0; col < numCols; col++ {
   221  			d := maxColWidths[col] - colWidths[col]
   222  			toAdd := w
   223  			if d < toAdd {
   224  				toAdd = d
   225  			}
   226  			colWidths[col] += toAdd
   227  			w -= toAdd
   228  			if w <= 0 {
   229  				break
   230  			}
   231  		}
   232  	}
   233  
   234  	return colWidths
   235  }
   236  
   237  type tsvTablePrinter struct {
   238  	out        io.Writer
   239  	currentCol int
   240  }
   241  
   242  func (t *tsvTablePrinter) AddHeader(_ []string, _ ...fieldOption) {}
   243  
   244  func (t *tsvTablePrinter) AddField(text string, _ ...fieldOption) {
   245  	if t.currentCol > 0 {
   246  		fmt.Fprint(t.out, "\t")
   247  	}
   248  	fmt.Fprint(t.out, text)
   249  	t.currentCol++
   250  }
   251  
   252  func (t *tsvTablePrinter) EndRow() {
   253  	fmt.Fprint(t.out, "\n")
   254  	t.currentCol = 0
   255  }
   256  
   257  func (t *tsvTablePrinter) Render() error {
   258  	return nil
   259  }
   260  

View as plain text