forked from toolshed/abra
		
	We were running behind and there were quite some deprecations to update. This was mostly in the upstream copy/pasta package but seems quite minimal.
		
			
				
	
	
		
			487 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			487 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package table
 | |
| 
 | |
| import (
 | |
| 	"strings"
 | |
| 
 | |
| 	"github.com/charmbracelet/lipgloss"
 | |
| 	"github.com/charmbracelet/x/ansi"
 | |
| )
 | |
| 
 | |
| // HeaderRow denotes the header's row index used when rendering headers. Use
 | |
| // this value when looking to customize header styles in StyleFunc.
 | |
| const HeaderRow int = -1
 | |
| 
 | |
| // StyleFunc is the style function that determines the style of a Cell.
 | |
| //
 | |
| // It takes the row and column of the cell as an input and determines the
 | |
| // lipgloss Style to use for that cell position.
 | |
| //
 | |
| // Example:
 | |
| //
 | |
| //	t := table.New().
 | |
| //	    Headers("Name", "Age").
 | |
| //	    Row("Kini", 4).
 | |
| //	    Row("Eli", 1).
 | |
| //	    Row("Iris", 102).
 | |
| //	    StyleFunc(func(row, col int) lipgloss.Style {
 | |
| //	        switch {
 | |
| //	           case row == 0:
 | |
| //	               return HeaderStyle
 | |
| //	           case row%2 == 0:
 | |
| //	               return EvenRowStyle
 | |
| //	           default:
 | |
| //	               return OddRowStyle
 | |
| //	           }
 | |
| //	    })
 | |
| type StyleFunc func(row, col int) lipgloss.Style
 | |
| 
 | |
| // DefaultStyles is a TableStyleFunc that returns a new Style with no attributes.
 | |
| func DefaultStyles(_, _ int) lipgloss.Style {
 | |
| 	return lipgloss.NewStyle()
 | |
| }
 | |
| 
 | |
| // Table is a type for rendering tables.
 | |
| type Table struct {
 | |
| 	styleFunc StyleFunc
 | |
| 	border    lipgloss.Border
 | |
| 
 | |
| 	borderTop    bool
 | |
| 	borderBottom bool
 | |
| 	borderLeft   bool
 | |
| 	borderRight  bool
 | |
| 	borderHeader bool
 | |
| 	borderColumn bool
 | |
| 	borderRow    bool
 | |
| 
 | |
| 	borderStyle lipgloss.Style
 | |
| 	headers     []string
 | |
| 	data        Data
 | |
| 
 | |
| 	width           int
 | |
| 	height          int
 | |
| 	useManualHeight bool
 | |
| 	offset          int
 | |
| 	wrap            bool
 | |
| 
 | |
| 	// widths tracks the width of each column.
 | |
| 	widths []int
 | |
| 
 | |
| 	// heights tracks the height of each row.
 | |
| 	heights []int
 | |
| }
 | |
| 
 | |
| // New returns a new Table that can be modified through different
 | |
| // attributes.
 | |
| //
 | |
| // By default, a table has no border, no styling, and no rows.
 | |
| func New() *Table {
 | |
| 	return &Table{
 | |
| 		styleFunc:    DefaultStyles,
 | |
| 		border:       lipgloss.RoundedBorder(),
 | |
| 		borderBottom: true,
 | |
| 		borderColumn: true,
 | |
| 		borderHeader: true,
 | |
| 		borderLeft:   true,
 | |
| 		borderRight:  true,
 | |
| 		borderTop:    true,
 | |
| 		wrap:         true,
 | |
| 		data:         NewStringData(),
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // ClearRows clears the table rows.
 | |
| func (t *Table) ClearRows() *Table {
 | |
| 	t.data = NewStringData()
 | |
| 	return t
 | |
| }
 | |
| 
 | |
| // StyleFunc sets the style for a cell based on it's position (row, column).
 | |
| func (t *Table) StyleFunc(style StyleFunc) *Table {
 | |
| 	t.styleFunc = style
 | |
| 	return t
 | |
| }
 | |
| 
 | |
| // style returns the style for a cell based on it's position (row, column).
 | |
| func (t *Table) style(row, col int) lipgloss.Style {
 | |
| 	if t.styleFunc == nil {
 | |
| 		return lipgloss.NewStyle()
 | |
| 	}
 | |
| 	return t.styleFunc(row, col)
 | |
| }
 | |
| 
 | |
| // Data sets the table data.
 | |
| func (t *Table) Data(data Data) *Table {
 | |
| 	t.data = data
 | |
| 	return t
 | |
| }
 | |
| 
 | |
| // Rows appends rows to the table data.
 | |
| func (t *Table) Rows(rows ...[]string) *Table {
 | |
| 	for _, row := range rows {
 | |
| 		switch t.data.(type) {
 | |
| 		case *StringData:
 | |
| 			t.data.(*StringData).Append(row)
 | |
| 		}
 | |
| 	}
 | |
| 	return t
 | |
| }
 | |
| 
 | |
| // Row appends a row to the table data.
 | |
| func (t *Table) Row(row ...string) *Table {
 | |
| 	switch t.data.(type) {
 | |
| 	case *StringData:
 | |
| 		t.data.(*StringData).Append(row)
 | |
| 	}
 | |
| 	return t
 | |
| }
 | |
| 
 | |
| // Headers sets the table headers.
 | |
| func (t *Table) Headers(headers ...string) *Table {
 | |
| 	t.headers = headers
 | |
| 	return t
 | |
| }
 | |
| 
 | |
| // Border sets the table border.
 | |
| func (t *Table) Border(border lipgloss.Border) *Table {
 | |
| 	t.border = border
 | |
| 	return t
 | |
| }
 | |
| 
 | |
| // BorderTop sets the top border.
 | |
| func (t *Table) BorderTop(v bool) *Table {
 | |
| 	t.borderTop = v
 | |
| 	return t
 | |
| }
 | |
| 
 | |
| // BorderBottom sets the bottom border.
 | |
| func (t *Table) BorderBottom(v bool) *Table {
 | |
| 	t.borderBottom = v
 | |
| 	return t
 | |
| }
 | |
| 
 | |
| // BorderLeft sets the left border.
 | |
| func (t *Table) BorderLeft(v bool) *Table {
 | |
| 	t.borderLeft = v
 | |
| 	return t
 | |
| }
 | |
| 
 | |
| // BorderRight sets the right border.
 | |
| func (t *Table) BorderRight(v bool) *Table {
 | |
| 	t.borderRight = v
 | |
| 	return t
 | |
| }
 | |
| 
 | |
| // BorderHeader sets the header separator border.
 | |
| func (t *Table) BorderHeader(v bool) *Table {
 | |
| 	t.borderHeader = v
 | |
| 	return t
 | |
| }
 | |
| 
 | |
| // BorderColumn sets the column border separator.
 | |
| func (t *Table) BorderColumn(v bool) *Table {
 | |
| 	t.borderColumn = v
 | |
| 	return t
 | |
| }
 | |
| 
 | |
| // BorderRow sets the row border separator.
 | |
| func (t *Table) BorderRow(v bool) *Table {
 | |
| 	t.borderRow = v
 | |
| 	return t
 | |
| }
 | |
| 
 | |
| // BorderStyle sets the style for the table border.
 | |
| func (t *Table) BorderStyle(style lipgloss.Style) *Table {
 | |
| 	t.borderStyle = style
 | |
| 	return t
 | |
| }
 | |
| 
 | |
| // Width sets the table width, this auto-sizes the columns to fit the width by
 | |
| // either expanding or contracting the widths of each column as a best effort
 | |
| // approach.
 | |
| func (t *Table) Width(w int) *Table {
 | |
| 	t.width = w
 | |
| 	return t
 | |
| }
 | |
| 
 | |
| // Height sets the table height.
 | |
| func (t *Table) Height(h int) *Table {
 | |
| 	t.height = h
 | |
| 	t.useManualHeight = true
 | |
| 	return t
 | |
| }
 | |
| 
 | |
| // Offset sets the table rendering offset.
 | |
| //
 | |
| // Warning: you may declare Offset only after setting Rows. Otherwise it will be
 | |
| // ignored.
 | |
| func (t *Table) Offset(o int) *Table {
 | |
| 	t.offset = o
 | |
| 	return t
 | |
| }
 | |
| 
 | |
| // Wrap dictates whether or not the table content should wrap.
 | |
| func (t *Table) Wrap(w bool) *Table {
 | |
| 	t.wrap = w
 | |
| 	return t
 | |
| }
 | |
| 
 | |
| // String returns the table as a string.
 | |
| func (t *Table) String() string {
 | |
| 	hasHeaders := len(t.headers) > 0
 | |
| 	hasRows := t.data != nil && t.data.Rows() > 0
 | |
| 
 | |
| 	if !hasHeaders && !hasRows {
 | |
| 		return ""
 | |
| 	}
 | |
| 
 | |
| 	// Add empty cells to the headers, until it's the same length as the longest
 | |
| 	// row (only if there are at headers in the first place).
 | |
| 	if hasHeaders {
 | |
| 		for i := len(t.headers); i < t.data.Columns(); i++ {
 | |
| 			t.headers = append(t.headers, "")
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Do all the sizing calculations for width and height.
 | |
| 	t.resize()
 | |
| 
 | |
| 	var sb strings.Builder
 | |
| 
 | |
| 	if t.borderTop {
 | |
| 		sb.WriteString(t.constructTopBorder())
 | |
| 		sb.WriteString("\n")
 | |
| 	}
 | |
| 
 | |
| 	if hasHeaders {
 | |
| 		sb.WriteString(t.constructHeaders())
 | |
| 		sb.WriteString("\n")
 | |
| 	}
 | |
| 
 | |
| 	var bottom string
 | |
| 	if t.borderBottom {
 | |
| 		bottom = t.constructBottomBorder()
 | |
| 	}
 | |
| 
 | |
| 	// If there are no data rows render nothing.
 | |
| 	if t.data.Rows() > 0 {
 | |
| 		switch {
 | |
| 		case t.useManualHeight:
 | |
| 			// The height of the top border. Subtract 1 for the newline.
 | |
| 			topHeight := lipgloss.Height(sb.String()) - 1
 | |
| 			availableLines := t.height - (topHeight + lipgloss.Height(bottom))
 | |
| 
 | |
| 			// if the height is larger than the number of rows, use the number
 | |
| 			// of rows.
 | |
| 			if availableLines > t.data.Rows() {
 | |
| 				availableLines = t.data.Rows()
 | |
| 			}
 | |
| 			sb.WriteString(t.constructRows(availableLines))
 | |
| 
 | |
| 		default:
 | |
| 			for r := t.offset; r < t.data.Rows(); r++ {
 | |
| 				sb.WriteString(t.constructRow(r, false))
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	sb.WriteString(bottom)
 | |
| 
 | |
| 	return lipgloss.NewStyle().
 | |
| 		MaxHeight(t.computeHeight()).
 | |
| 		MaxWidth(t.width).
 | |
| 		Render(sb.String())
 | |
| }
 | |
| 
 | |
| // computeHeight computes the height of the table in it's current configuration.
 | |
| func (t *Table) computeHeight() int {
 | |
| 	hasHeaders := len(t.headers) > 0
 | |
| 	return sum(t.heights) - 1 + btoi(hasHeaders) +
 | |
| 		btoi(t.borderTop) + btoi(t.borderBottom) +
 | |
| 		btoi(t.borderHeader) + t.data.Rows()*btoi(t.borderRow)
 | |
| }
 | |
| 
 | |
| // Render returns the table as a string.
 | |
| func (t *Table) Render() string {
 | |
| 	return t.String()
 | |
| }
 | |
| 
 | |
| // constructTopBorder constructs the top border for the table given it's current
 | |
| // border configuration and data.
 | |
| func (t *Table) constructTopBorder() string {
 | |
| 	var s strings.Builder
 | |
| 	if t.borderLeft {
 | |
| 		s.WriteString(t.borderStyle.Render(t.border.TopLeft))
 | |
| 	}
 | |
| 	for i := 0; i < len(t.widths); i++ {
 | |
| 		s.WriteString(t.borderStyle.Render(strings.Repeat(t.border.Top, t.widths[i])))
 | |
| 		if i < len(t.widths)-1 && t.borderColumn {
 | |
| 			s.WriteString(t.borderStyle.Render(t.border.MiddleTop))
 | |
| 		}
 | |
| 	}
 | |
| 	if t.borderRight {
 | |
| 		s.WriteString(t.borderStyle.Render(t.border.TopRight))
 | |
| 	}
 | |
| 	return s.String()
 | |
| }
 | |
| 
 | |
| // constructBottomBorder constructs the bottom border for the table given it's current
 | |
| // border configuration and data.
 | |
| func (t *Table) constructBottomBorder() string {
 | |
| 	var s strings.Builder
 | |
| 	if t.borderLeft {
 | |
| 		s.WriteString(t.borderStyle.Render(t.border.BottomLeft))
 | |
| 	}
 | |
| 	for i := 0; i < len(t.widths); i++ {
 | |
| 		s.WriteString(t.borderStyle.Render(strings.Repeat(t.border.Bottom, t.widths[i])))
 | |
| 		if i < len(t.widths)-1 && t.borderColumn {
 | |
| 			s.WriteString(t.borderStyle.Render(t.border.MiddleBottom))
 | |
| 		}
 | |
| 	}
 | |
| 	if t.borderRight {
 | |
| 		s.WriteString(t.borderStyle.Render(t.border.BottomRight))
 | |
| 	}
 | |
| 	return s.String()
 | |
| }
 | |
| 
 | |
| // constructHeaders constructs the headers for the table given it's current
 | |
| // header configuration and data.
 | |
| func (t *Table) constructHeaders() string {
 | |
| 	var s strings.Builder
 | |
| 	if t.borderLeft {
 | |
| 		s.WriteString(t.borderStyle.Render(t.border.Left))
 | |
| 	}
 | |
| 	for i, header := range t.headers {
 | |
| 		s.WriteString(t.style(HeaderRow, i).
 | |
| 			MaxHeight(1).
 | |
| 			Width(t.widths[i]).
 | |
| 			MaxWidth(t.widths[i]).
 | |
| 			Render(ansi.Truncate(header, t.widths[i], "…")))
 | |
| 		if i < len(t.headers)-1 && t.borderColumn {
 | |
| 			s.WriteString(t.borderStyle.Render(t.border.Left))
 | |
| 		}
 | |
| 	}
 | |
| 	if t.borderHeader {
 | |
| 		if t.borderRight {
 | |
| 			s.WriteString(t.borderStyle.Render(t.border.Right))
 | |
| 		}
 | |
| 		s.WriteString("\n")
 | |
| 		if t.borderLeft {
 | |
| 			s.WriteString(t.borderStyle.Render(t.border.MiddleLeft))
 | |
| 		}
 | |
| 		for i := 0; i < len(t.headers); i++ {
 | |
| 			s.WriteString(t.borderStyle.Render(strings.Repeat(t.border.Top, t.widths[i])))
 | |
| 			if i < len(t.headers)-1 && t.borderColumn {
 | |
| 				s.WriteString(t.borderStyle.Render(t.border.Middle))
 | |
| 			}
 | |
| 		}
 | |
| 		if t.borderRight {
 | |
| 			s.WriteString(t.borderStyle.Render(t.border.MiddleRight))
 | |
| 		}
 | |
| 	}
 | |
| 	if t.borderRight && !t.borderHeader {
 | |
| 		s.WriteString(t.borderStyle.Render(t.border.Right))
 | |
| 	}
 | |
| 	return s.String()
 | |
| }
 | |
| 
 | |
| func (t *Table) constructRows(availableLines int) string {
 | |
| 	var sb strings.Builder
 | |
| 
 | |
| 	// The number of rows to render after removing the offset.
 | |
| 	offsetRowCount := t.data.Rows() - t.offset
 | |
| 
 | |
| 	// The number of rows to render. We always render at least one row.
 | |
| 	rowsToRender := availableLines
 | |
| 	rowsToRender = max(rowsToRender, 1)
 | |
| 
 | |
| 	// Check if we need to render an overflow row.
 | |
| 	needsOverflow := rowsToRender < offsetRowCount
 | |
| 
 | |
| 	// only use the offset as the starting value if there is overflow.
 | |
| 	rowIdx := t.offset
 | |
| 	if !needsOverflow {
 | |
| 		// if there is no overflow, just render to the height of the table
 | |
| 		// check there's enough content to fill the table
 | |
| 		rowIdx = t.data.Rows() - rowsToRender
 | |
| 	}
 | |
| 	for rowsToRender > 0 && rowIdx < t.data.Rows() {
 | |
| 		// Whenever the height is too small to render all rows, the bottom row will be an overflow row (ellipsis).
 | |
| 		isOverflow := needsOverflow && rowsToRender == 1
 | |
| 
 | |
| 		sb.WriteString(t.constructRow(rowIdx, isOverflow))
 | |
| 
 | |
| 		rowIdx++
 | |
| 		rowsToRender--
 | |
| 	}
 | |
| 	return sb.String()
 | |
| }
 | |
| 
 | |
| // constructRow constructs the row for the table given an index and row data
 | |
| // based on the current configuration. If isOverflow is true, the row is
 | |
| // rendered as an overflow row (using ellipsis).
 | |
| func (t *Table) constructRow(index int, isOverflow bool) string {
 | |
| 	var s strings.Builder
 | |
| 
 | |
| 	hasHeaders := len(t.headers) > 0
 | |
| 	height := t.heights[index+btoi(hasHeaders)]
 | |
| 	if isOverflow {
 | |
| 		height = 1
 | |
| 	}
 | |
| 
 | |
| 	var cells []string
 | |
| 	left := strings.Repeat(t.borderStyle.Render(t.border.Left)+"\n", height)
 | |
| 	if t.borderLeft {
 | |
| 		cells = append(cells, left)
 | |
| 	}
 | |
| 
 | |
| 	for c := 0; c < t.data.Columns(); c++ {
 | |
| 		cellWidth := t.widths[c]
 | |
| 
 | |
| 		cell := "…"
 | |
| 		if !isOverflow {
 | |
| 			cell = t.data.At(index, c)
 | |
| 		}
 | |
| 
 | |
| 		cellStyle := t.style(index, c)
 | |
| 		if !t.wrap {
 | |
| 			length := (cellWidth * height) - cellStyle.GetHorizontalPadding()
 | |
| 			cell = ansi.Truncate(cell, length, "…")
 | |
| 		}
 | |
| 		cells = append(cells, cellStyle.
 | |
| 			// Account for the margins in the cell sizing.
 | |
| 			Height(height-cellStyle.GetVerticalMargins()).
 | |
| 			MaxHeight(height).
 | |
| 			Width(t.widths[c]-cellStyle.GetHorizontalMargins()).
 | |
| 			MaxWidth(t.widths[c]).
 | |
| 			Render(cell))
 | |
| 
 | |
| 		if c < t.data.Columns()-1 && t.borderColumn {
 | |
| 			cells = append(cells, left)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if t.borderRight {
 | |
| 		right := strings.Repeat(t.borderStyle.Render(t.border.Right)+"\n", height)
 | |
| 		cells = append(cells, right)
 | |
| 	}
 | |
| 
 | |
| 	for i, cell := range cells {
 | |
| 		cells[i] = strings.TrimRight(cell, "\n")
 | |
| 	}
 | |
| 
 | |
| 	s.WriteString(lipgloss.JoinHorizontal(lipgloss.Top, cells...) + "\n")
 | |
| 
 | |
| 	if t.borderRow && index < t.data.Rows()-1 {
 | |
| 		s.WriteString(t.borderStyle.Render(t.border.MiddleLeft))
 | |
| 		for i := 0; i < len(t.widths); i++ {
 | |
| 			s.WriteString(t.borderStyle.Render(strings.Repeat(t.border.Bottom, t.widths[i])))
 | |
| 			if i < len(t.widths)-1 && t.borderColumn {
 | |
| 				s.WriteString(t.borderStyle.Render(t.border.Middle))
 | |
| 			}
 | |
| 		}
 | |
| 		s.WriteString(t.borderStyle.Render(t.border.MiddleRight) + "\n")
 | |
| 	}
 | |
| 
 | |
| 	return s.String()
 | |
| }
 |