0
0
forked from toolshed/abra
decentral1se 1723025fbf
build: go 1.24
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.
2025-03-16 12:31:45 +01:00

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