go.charczuk.com@v0.0.0-20240327042549-bc490516bd1a/sdk/ansi/table.go (about)

     1  /*
     2  
     3  Copyright (c) 2023 - Present. Will Charczuk. All rights reserved.
     4  Use of this source code is governed by a MIT license that can be found in the LICENSE file at the root of the repository.
     5  
     6  */
     7  
     8  package ansi
     9  
    10  import (
    11  	"errors"
    12  	"fmt"
    13  	"io"
    14  	"reflect"
    15  	"strings"
    16  	"unicode"
    17  	"unicode/utf8"
    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 errors.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  	var columns []string
    56  	for index := 0; index < ct.NumField(); index++ {
    57  		name := ct.Field(index).Name
    58  		if name == "" || unicode.IsLower(firstRune(name)) {
    59  			continue
    60  		}
    61  		columns = append(columns, name)
    62  	}
    63  
    64  	var rows [][]string
    65  	var rowValue reflect.Value
    66  	var field reflect.Value
    67  	for row := 0; row < cv.Len(); row++ {
    68  		rowValue = cv.Index(row)
    69  		rowValues := make([]string, len(columns))
    70  		for columnIndex := 0; columnIndex < len(columns); columnIndex++ {
    71  			field = rowValue.FieldByName(columns[columnIndex])
    72  			rowValues[columnIndex] = fmt.Sprintf("%v", field.Interface())
    73  		}
    74  		rows = append(rows, rowValues)
    75  	}
    76  
    77  	return Table(wr, columns, rows)
    78  }
    79  
    80  func firstRune(str string) (out rune) {
    81  	if str == "" {
    82  		return
    83  	}
    84  	for _, out = range str {
    85  		break
    86  	}
    87  	return
    88  }
    89  
    90  // Table writes a table to a given writer.
    91  func Table(wr io.Writer, columns []string, rows [][]string) (err error) {
    92  	if len(columns) == 0 {
    93  		return errors.New("table; invalid columns; column set is empty")
    94  	}
    95  
    96  	/* helpers */
    97  	defer func() {
    98  		if r := recover(); r != nil {
    99  			if typed, ok := r.(error); ok {
   100  				err = typed
   101  			}
   102  		}
   103  	}()
   104  	write := func(str string) {
   105  		_, writeErr := io.WriteString(wr, str)
   106  		if writeErr != nil {
   107  			panic(writeErr)
   108  		}
   109  	}
   110  	/* end helpers */
   111  
   112  	/* begin establish max widths of columns */
   113  	maxWidths := make([]int, len(columns))
   114  	for index, columnName := range columns {
   115  		maxWidths[index] = stringWidth(columnName)
   116  	}
   117  
   118  	var width int
   119  	for _, cols := range rows {
   120  		for index, columnValue := range cols {
   121  			width = stringWidth(columnValue)
   122  			if maxWidths[index] < width {
   123  				maxWidths[index] = width
   124  			}
   125  		}
   126  	}
   127  	/* end establish max widths of columns */
   128  
   129  	/* draw top of column row */
   130  	write(TableTopLeft)
   131  	for index := range columns {
   132  		write(repeat(TableHorizBar, maxWidths[index]))
   133  		if isNotLast(index, columns) {
   134  			write(TableTopSep)
   135  		}
   136  	}
   137  	write(TableTopRight)
   138  	write(NewLine)
   139  	/* end draw top of column row */
   140  
   141  	/* draw column names */
   142  	write(TableVertBar)
   143  	for index, columnLabel := range columns {
   144  		write(padRight(columnLabel, maxWidths[index]))
   145  		if isNotLast(index, columns) {
   146  			write(TableVertBar)
   147  		}
   148  	}
   149  	write(TableVertBar)
   150  	write(NewLine)
   151  	/* end draw column names */
   152  
   153  	/* draw bottom of column row */
   154  	write(TableMidLeft)
   155  	for index := range columns {
   156  		write(repeat(TableHorizBar, maxWidths[index]))
   157  		if isNotLast(index, columns) {
   158  			write(TableMidSep)
   159  		}
   160  	}
   161  	write(TableMidRight)
   162  	write(NewLine)
   163  	/* end draw bottom of column row */
   164  
   165  	/* draw rows */
   166  	for _, row := range rows {
   167  		write(TableVertBar)
   168  		for index, column := range row {
   169  			write(padRight(column, maxWidths[index]))
   170  			if isNotLast(index, columns) {
   171  				write(TableVertBar)
   172  			}
   173  		}
   174  		write(TableVertBar)
   175  		write(NewLine)
   176  	}
   177  	/* end draw rows */
   178  
   179  	/* draw footer */
   180  	write(TableBottomLeft)
   181  	for index := range columns {
   182  		write(repeat(TableHorizBar, maxWidths[index]))
   183  		if isNotLast(index, columns) {
   184  			write(TableBottomSep)
   185  		}
   186  	}
   187  	write(TableBottomRight)
   188  	write(NewLine)
   189  	/* end draw footer */
   190  	return
   191  }
   192  
   193  func stringWidth(value string) (width int) {
   194  	var runeWidth int
   195  	for _, c := range value {
   196  		runeWidth = utf8.RuneLen(c)
   197  		if runeWidth > 1 {
   198  			width += 2
   199  		} else {
   200  			width++
   201  		}
   202  	}
   203  	return
   204  }
   205  
   206  func repeat(str string, count int) string {
   207  	return strings.Repeat(str, count)
   208  }
   209  
   210  func padRight(value string, width int) string {
   211  	valueWidth := stringWidth(value)
   212  	spaces := width - valueWidth
   213  	if spaces == 0 {
   214  		return value
   215  	}
   216  	return value + strings.Repeat(" ", spaces)
   217  }
   218  
   219  func isNotLast(index int, values []string) bool {
   220  	return index < (len(values) - 1)
   221  }