Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions examples/table/footer/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package main

import (
"fmt"

"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/table"
)

func main() {
tbl := table.New().
Headers("Name", "Age", "City").
Row("Alice", "25", "New York").
Row("Bob", "30", "Los Angeles").
Row("Charlie", "35", "Chicago").
Footers("Total", "3", "3 cities").
StyleFunc(func(row, col int) lipgloss.Style {
if row == table.HeaderRow {
return lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#4A90E2"))
}
if row == table.FooterRow {
return lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color("#FFFFFF")).
Background(lipgloss.Color("#34495E"))
}
return lipgloss.NewStyle()
}).
Border(lipgloss.RoundedBorder()).
BorderHeader(true).
BorderFooter(true).
BorderColumn(true).
BorderRow(false)

fmt.Println(tbl)
}
18 changes: 14 additions & 4 deletions table/resizing.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,9 @@ import (
// The biggest difference is 15 - 2, so we can shrink the 2nd column by 13.
func (t *Table) resize() {
hasHeaders := len(t.headers) > 0
hasFooters := len(t.footers) > 0
rows := dataToMatrix(t.data)
r := newResizer(t.width, t.height, t.headers, rows)
r := newResizer(t.width, t.height, t.headers, rows, t.footers)
r.wrap = t.wrap
r.borderColumn = t.borderColumn
r.yPaddings = make([][]int, len(r.allRows))
Expand All @@ -58,6 +59,9 @@ func (t *Table) resize() {
} else {
allRows = rows
}
if hasFooters {
allRows = append(allRows, t.footers)
}

styleFunc := t.styleFunc
if t.styleFunc == nil {
Expand All @@ -72,12 +76,15 @@ func (t *Table) resize() {
for j := range row {
column := &r.columns[j]

// Making sure we're passing the right index to `styleFunc`. The header row should be `-1` and
// the others should start from `0`.
// Making sure we're passing the right index to `styleFunc`. The header row should be `-1`,
// footer row should be `-2`, and the others should start from `0`.
rowIndex := i
if hasHeaders {
rowIndex--
}
if hasFooters && i >= len(rows)+btoi(hasHeaders) {
rowIndex = FooterRow
}
style := styleFunc(rowIndex, j)

topMargin, rightMargin, bottomMargin, leftMargin := style.GetMargin()
Expand Down Expand Up @@ -129,7 +136,7 @@ type resizer struct {
}

// newResizer creates a new resizer.
func newResizer(tableWidth, tableHeight int, headers []string, rows [][]string) *resizer {
func newResizer(tableWidth, tableHeight int, headers []string, rows [][]string, footers []string) *resizer {
r := &resizer{
tableWidth: tableWidth,
tableHeight: tableHeight,
Expand All @@ -141,6 +148,9 @@ func newResizer(tableWidth, tableHeight int, headers []string, rows [][]string)
} else {
r.allRows = rows
}
if len(footers) > 0 {
r.allRows = append(r.allRows, footers)
}

for _, row := range r.allRows {
for i, cell := range row {
Expand Down
99 changes: 95 additions & 4 deletions table/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import (
// this value when looking to customize header styles in StyleFunc.
const HeaderRow int = -1

// FooterRow denotes the footer's row index used when rendering footers. Use
// this value when looking to customize footer styles in StyleFunc.
const FooterRow int = -2

// 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
Expand Down Expand Up @@ -51,11 +55,13 @@ type Table struct {
borderLeft bool
borderRight bool
borderHeader bool
borderFooter bool
borderColumn bool
borderRow bool

borderStyle lipgloss.Style
headers []string
footers []string
data Data

width int
Expand All @@ -82,6 +88,7 @@ func New() *Table {
borderBottom: true,
borderColumn: true,
borderHeader: true,
borderFooter: true,
borderLeft: true,
borderRight: true,
borderTop: true,
Expand Down Expand Up @@ -142,6 +149,12 @@ func (t *Table) Headers(headers ...string) *Table {
return t
}

// Footers sets the table footers.
func (t *Table) Footers(footers ...string) *Table {
t.footers = footers
return t
}

// Border sets the table border.
func (t *Table) Border(border lipgloss.Border) *Table {
t.border = border
Expand Down Expand Up @@ -178,6 +191,12 @@ func (t *Table) BorderHeader(v bool) *Table {
return t
}

// BorderFooter sets the footer separator border.
func (t *Table) BorderFooter(v bool) *Table {
t.borderFooter = v
return t
}

// BorderColumn sets the column border separator.
func (t *Table) BorderColumn(v bool) *Table {
t.borderColumn = v
Expand Down Expand Up @@ -229,9 +248,10 @@ func (t *Table) Wrap(w bool) *Table {
// String returns the table as a string.
func (t *Table) String() string {
hasHeaders := len(t.headers) > 0
hasFooters := len(t.footers) > 0
hasRows := t.data != nil && t.data.Rows() > 0

if !hasHeaders && !hasRows {
if !hasHeaders && !hasFooters && !hasRows {
return ""
}

Expand All @@ -243,6 +263,14 @@ func (t *Table) String() string {
}
}

// Add empty cells to the footers, until it's the same length as the longest
// row (only if there are footers in the first place).
if hasFooters {
for i := len(t.footers); i < t.data.Columns(); i++ {
t.footers = append(t.footers, "")
}
}

// Do all the sizing calculations for width and height.
t.resize()

Expand Down Expand Up @@ -285,6 +313,11 @@ func (t *Table) String() string {
}
}

if hasFooters {
sb.WriteString(t.constructFooters())
sb.WriteString("\n")
}

sb.WriteString(bottom)

return lipgloss.NewStyle().
Expand All @@ -296,9 +329,10 @@ func (t *Table) String() 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) +
hasFooters := len(t.footers) > 0
return sum(t.heights) - 1 + btoi(hasHeaders) + btoi(hasFooters) +
btoi(t.borderTop) + btoi(t.borderBottom) +
btoi(t.borderHeader) + t.data.Rows()*btoi(t.borderRow)
btoi(t.borderHeader) + btoi(hasFooters && t.borderFooter) + t.data.Rows()*btoi(t.borderRow)
}

// Render returns the table as a string.
Expand Down Expand Up @@ -394,6 +428,55 @@ func (t *Table) constructHeaders() string {
return s.String()
}

// constructFooters constructs the footers for the table given it's current
// footer configuration and data.
func (t *Table) constructFooters() string {
// Footer row is at the end of the heights array
footerRowIndex := len(t.heights) - 1
height := t.heights[footerRowIndex]

var s strings.Builder
if t.borderFooter {
if t.borderLeft {
s.WriteString(t.borderStyle.Render(t.border.MiddleLeft))
}
for i := 0; i < len(t.footers); i++ {
s.WriteString(t.borderStyle.Render(strings.Repeat(t.border.Bottom, t.widths[i])))
if i < len(t.footers)-1 && t.borderColumn {
s.WriteString(t.borderStyle.Render(t.border.Middle))
}
}
if t.borderRight {
s.WriteString(t.borderStyle.Render(t.border.MiddleRight))
}
s.WriteString("\n")
}
if t.borderLeft {
s.WriteString(t.borderStyle.Render(t.border.Left))
}
for i, footer := range t.footers {
cellStyle := t.style(FooterRow, i)

if !t.wrap {
footer = t.truncateCell(footer, FooterRow, i)
}

s.WriteString(cellStyle.
Height(height - cellStyle.GetVerticalMargins()).
MaxHeight(height).
Width(t.widths[i] - cellStyle.GetHorizontalMargins()).
MaxWidth(t.widths[i]).
Render(t.truncateCell(footer, FooterRow, i)))
if i < len(t.footers)-1 && t.borderColumn {
s.WriteString(t.borderStyle.Render(t.border.Left))
}
}
if t.borderRight {
s.WriteString(t.borderStyle.Render(t.border.Right))
}
return s.String()
}

func (t *Table) constructRows(availableLines int) string {
var sb strings.Builder

Expand Down Expand Up @@ -494,7 +577,15 @@ func (t *Table) constructRow(index int, isOverflow bool) string {

func (t *Table) truncateCell(cell string, rowIndex, colIndex int) string {
hasHeaders := len(t.headers) > 0
height := t.heights[rowIndex+btoi(hasHeaders)]

var height int
if rowIndex == FooterRow {
// Footer row is at the end of the heights array
height = t.heights[len(t.heights)-1]
} else {
height = t.heights[rowIndex+btoi(hasHeaders)]
}

cellWidth := t.widths[colIndex]
cellStyle := t.style(rowIndex, colIndex)

Expand Down
15 changes: 15 additions & 0 deletions table/table_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1407,3 +1407,18 @@ func TestWrapStyleFuncContent(t *testing.T) {
Wrap(true)
golden.RequireEqual(t, []byte(table.String()))
}

func TestTableFooters(t *testing.T) {
tbl := New().
Headers("Name", "Age", "City").
Row("Alice", "25", "New York").
Row("Bob", "30", "Los Angeles").
Row("Charlie", "35", "Chicago").
Footers("Total", "3", "3 cities").
StyleFunc(func(row, col int) lipgloss.Style {
return lipgloss.NewStyle()
}).
Border(lipgloss.RoundedBorder())

golden.RequireEqual(t, []byte(tbl.String()))
}
9 changes: 9 additions & 0 deletions table/testdata/TestTableFooters.golden

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.