github.com/blend/go-sdk@v1.20220411.3/ansi/table.go (about)

     1  /*
     2  
     3  Copyright (c) 2022 - Present. Blend Labs, Inc. All rights reserved
     4  Use of this source code is governed by a MIT license that can be found in the LICENSE file.
     5  
     6  */
     7  
     8  package ansi
     9  
    10  import (
    11  	"fmt"
    12  	"io"
    13  	"reflect"
    14  	"strings"
    15  	"unicode/utf8"
    16  
    17  	"github.com/blend/go-sdk/ex"
    18  )
    19  
    20  // Table character constants.
    21  const (
    22  	TableTopLeft     = "┌"
    23  	TableTopRight    = "┐"
    24  	TableBottomLeft  = "└"
    25  	TableBottomRight = "┘"
    26  	TableMidLeft     = "├"
    27  	TableMidRight    = "┤"
    28  	TableVertBar     = "│"
    29  	TableHorizBar    = "─"
    30  	TableTopSep      = "┬"
    31  	TableBottomSep   = "┴"
    32  	TableMidSep      = "┼"
    33  	NewLine          = "\n"
    34  )
    35  
    36  // TableForSlice prints a table for a given slice.
    37  // It will infer column names from the struct fields.
    38  // If it is a mixed array (i.e. []interface{}) it will probably panic.
    39  func TableForSlice(wr io.Writer, collection interface{}) error {
    40  	// infer the column names from the fields
    41  	cv := reflect.ValueOf(collection)
    42  	for cv.Kind() == reflect.Ptr {
    43  		cv = cv.Elem()
    44  	}
    45  
    46  	if cv.Kind() != reflect.Slice {
    47  		return ex.New("table for slice; cannot iterate over non-slice collection")
    48  	}
    49  
    50  	ct := cv.Type()
    51  	for ct.Kind() == reflect.Ptr || ct.Kind() == reflect.Slice {
    52  		ct = ct.Elem()
    53  	}
    54  
    55  	columns := make([]string, ct.NumField())
    56  	for index := 0; index < ct.NumField(); index++ {
    57  		columns[index] = ct.Field(index).Name
    58  	}
    59  
    60  	var rows [][]string
    61  	var rowValue reflect.Value
    62  	for row := 0; row < cv.Len(); row++ {
    63  		rowValue = cv.Index(row)
    64  		rowValues := make([]string, ct.NumField())
    65  		for fieldIndex := 0; fieldIndex < ct.NumField(); fieldIndex++ {
    66  			rowValues[fieldIndex] = fmt.Sprintf("%v", rowValue.Field(fieldIndex).Interface())
    67  		}
    68  		rows = append(rows, rowValues)
    69  	}
    70  
    71  	return Table(wr, columns, rows)
    72  }
    73  
    74  // Table writes a table to a given writer.
    75  func Table(wr io.Writer, columns []string, rows [][]string) (err error) {
    76  	if len(columns) == 0 {
    77  		return ex.New("table; invalid columns; column set is empty")
    78  	}
    79  
    80  	/* helpers */
    81  	defer func() {
    82  		if r := recover(); r != nil {
    83  			if typed, ok := r.(error); ok {
    84  				err = typed
    85  			}
    86  		}
    87  	}()
    88  	write := func(str string) {
    89  		_, writeErr := io.WriteString(wr, str)
    90  		if writeErr != nil {
    91  			panic(writeErr)
    92  		}
    93  	}
    94  	/* end helpers */
    95  
    96  	/* begin establish max widths of columns */
    97  	maxWidths := make([]int, len(columns))
    98  	for index, columnName := range columns {
    99  		maxWidths[index] = stringWidth(columnName)
   100  	}
   101  
   102  	var width int
   103  	for _, cols := range rows {
   104  		for index, columnValue := range cols {
   105  			width = stringWidth(columnValue)
   106  			if maxWidths[index] < width {
   107  				maxWidths[index] = width
   108  			}
   109  		}
   110  	}
   111  	/* end establish max widths of columns */
   112  
   113  	/* draw top of column row */
   114  	write(TableTopLeft)
   115  	for index := range columns {
   116  		write(repeat(TableHorizBar, maxWidths[index]))
   117  		if isNotLast(index, columns) {
   118  			write(TableTopSep)
   119  		}
   120  	}
   121  	write(TableTopRight)
   122  	write(NewLine)
   123  	/* end draw top of column row */
   124  
   125  	/* draw column names */
   126  	write(TableVertBar)
   127  	for index, columnLabel := range columns {
   128  		write(padRight(columnLabel, maxWidths[index]))
   129  		if isNotLast(index, columns) {
   130  			write(TableVertBar)
   131  		}
   132  	}
   133  	write(TableVertBar)
   134  	write(NewLine)
   135  	/* end draw column names */
   136  
   137  	/* draw bottom of column row */
   138  	write(TableMidLeft)
   139  	for index := range columns {
   140  		write(repeat(TableHorizBar, maxWidths[index]))
   141  		if isNotLast(index, columns) {
   142  			write(TableMidSep)
   143  		}
   144  	}
   145  	write(TableMidRight)
   146  	write(NewLine)
   147  	/* end draw bottom of column row */
   148  
   149  	/* draw rows */
   150  	for _, row := range rows {
   151  		write(TableVertBar)
   152  		for index, column := range row {
   153  			write(padRight(column, maxWidths[index]))
   154  			if isNotLast(index, columns) {
   155  				write(TableVertBar)
   156  			}
   157  		}
   158  		write(TableVertBar)
   159  		write(NewLine)
   160  	}
   161  	/* end draw rows */
   162  
   163  	/* draw footer */
   164  	write(TableBottomLeft)
   165  	for index := range columns {
   166  		write(repeat(TableHorizBar, maxWidths[index]))
   167  		if isNotLast(index, columns) {
   168  			write(TableBottomSep)
   169  		}
   170  	}
   171  	write(TableBottomRight)
   172  	write(NewLine)
   173  	/* end draw footer */
   174  	return
   175  }
   176  
   177  func stringWidth(value string) (width int) {
   178  	var runeWidth int
   179  	for _, c := range value {
   180  		runeWidth = utf8.RuneLen(c)
   181  		if runeWidth > 1 {
   182  			width += 2
   183  		} else {
   184  			width++
   185  		}
   186  	}
   187  	return
   188  }
   189  
   190  func repeat(str string, count int) string {
   191  	return strings.Repeat(str, count)
   192  }
   193  
   194  func padRight(value string, width int) string {
   195  	valueWidth := stringWidth(value)
   196  	spaces := width - valueWidth
   197  	if spaces == 0 {
   198  		return value
   199  	}
   200  	return value + strings.Repeat(" ", spaces)
   201  }
   202  
   203  func isNotLast(index int, values []string) bool {
   204  	return index < (len(values) - 1)
   205  }