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()
|
|
}
|