Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(table): supports custom table border styles for each part #250

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
68 changes: 68 additions & 0 deletions examples/table/border/main.go
@@ -0,0 +1,68 @@
package main

import (
"fmt"
"os"

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

func main() {
re := lipgloss.NewRenderer(os.Stdout)

var (
HeaderStyle = re.NewStyle().Foreground(lipgloss.Color("1")).Bold(true).Align(lipgloss.Center)
CellStyle = re.NewStyle().Padding(0, 1)
DefaultCellStyle = CellStyle.Copy().Foreground(lipgloss.Color("6"))
SelectedCellStyle = CellStyle.Copy().Foreground(lipgloss.Color("5")).Bold(true)
DefaultBorderStyle = re.NewStyle().Foreground(lipgloss.Color("8")).Faint(true)
SelectedBorderStyle = re.NewStyle().Foreground(lipgloss.Color("10")).Bold(true)
)

rows := [][]string{
{"English", "Hello", "Hi"},
{"Chinese", "您好", "你好"},
{"Japanese", "こんにちは", "やあ"},
{"Arabic", "أهلين", "أهلا"},
{"Russian", "Здравствуйте", "Привет"},
{"Spanish", "Hola", "¿Qué tal?"},
}

for idx := range rows {
t := table.New().
BorderRow(true).
StyleFunc(func(row, col int) lipgloss.Style {
if row == 0 {
return HeaderStyle
}
if row == idx+1 {
return SelectedCellStyle
}
return DefaultCellStyle
}).
BorderStyleFunc(func(row, col int, borderType table.BorderType) lipgloss.Style {
if row == idx {
switch borderType {
case table.BorderBottom:
return SelectedBorderStyle
}
} else if row == idx+1 {
switch borderType {
case table.BorderLeft:
if col == 0 {
return SelectedBorderStyle
}
case table.BorderRight, table.BorderBottom:
return SelectedBorderStyle
}
}
return DefaultBorderStyle
}).
Headers("LANGUAGE", "FORMAL", "INFORMAL").
Rows(rows...)

fmt.Println(t)
fmt.Println()
}
}
133 changes: 102 additions & 31 deletions table/table.go
Expand Up @@ -31,15 +31,39 @@ import (
// })
type StyleFunc func(row, col int) lipgloss.Style

// BorderType contains a series of values which comprise the various parts of a border.
type BorderType int

// A series of BorderType which comprise the various parts of a border.
const (
BorderTop BorderType = iota
BorderBottom
BorderLeft
BorderRight
BorderTopLeft
BorderTopRight
BorderBottomLeft
BorderBottomRight
BorderMiddleLeft
BorderMiddleRight
BorderMiddle
BorderMiddleTop
BorderMiddleBottom
)

// BorderStyleFunc is the style function that determines the style of a cell border.
type BorderStyleFunc func(row, col int, borderType BorderType) 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
styleFunc StyleFunc
borderStyleFunc BorderStyleFunc
border lipgloss.Border

borderTop bool
borderBottom bool
Expand All @@ -62,6 +86,8 @@ type Table struct {

// heights tracks the height of each row.
heights []int

fixedColumns []int
}

// New returns a new Table that can be modified through different
Expand Down Expand Up @@ -102,6 +128,20 @@ func (t *Table) style(row, col int) lipgloss.Style {
return t.styleFunc(row, col)
}

// BorderStyleFunc sets the style for a cell border based on it's position (row, column).
func (t *Table) BorderStyleFunc(style BorderStyleFunc) *Table {
t.borderStyleFunc = style
return t
}

// getBorderStyle returns the style for a cell border based on it's position (row, column).
func (t *Table) getBorderStyle(row, col int, borderType BorderType) lipgloss.Style {
if t.borderStyleFunc == nil {
return t.borderStyle
}
return t.borderStyleFunc(row, col, borderType)
}

// Data sets the table data.
func (t *Table) Data(data Data) *Table {
t.data = data
Expand Down Expand Up @@ -290,11 +330,14 @@ func (t *Table) String() string {
if width < t.width && t.width > 0 {
// Table is too narrow, expand the columns evenly until it reaches the
// desired width.
var i int
for width < t.width {
t.widths[i]++
width++
i = (i + 1) % len(t.widths)
idx := t.getExpandableColumns()
if len(idx) > 0 {
var i int
for width < t.width {
t.widths[idx[i]]++
width++
i = (i + 1) % len(idx)
}
}
} else if width > t.width && t.width > 0 {
// Table is too wide, calculate the median non-whitespace length of each
Expand Down Expand Up @@ -365,6 +408,35 @@ func (t *Table) String() string {
MaxWidth(t.width).Render(s.String())
}

// GetTotalWidth returns the total width of the table, usually called after String.
func (t *Table) GetTotalWidth() int {
return t.computeWidth()
}

// FixedColumns make sure the columns not to be expanded when table is too narrow.
func (t *Table) FixedColumns(columns ...int) *Table {
t.fixedColumns = append(t.fixedColumns, columns...)
return t
}

// getExpandableColumns returns the non-fixed columns.
func (t *Table) getExpandableColumns() []int {
var idx []int
for i := 0; i < len(t.widths); i++ {
fixed := false
for _, j := range t.fixedColumns {
if i == j {
fixed = true
break
}
}
if !fixed {
idx = append(idx, i)
}
}
return idx
}

// computeWidth computes the width of the table in it's current configuration.
func (t *Table) computeWidth() int {
width := sum(t.widths) + btoi(t.borderLeft) + btoi(t.borderRight)
Expand Down Expand Up @@ -392,16 +464,16 @@ func (t *Table) Render() string {
func (t *Table) constructTopBorder() string {
var s strings.Builder
if t.borderLeft {
s.WriteString(t.borderStyle.Render(t.border.TopLeft))
s.WriteString(t.getBorderStyle(0, 0, BorderTopLeft).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])))
s.WriteString(t.getBorderStyle(0, i, BorderTop).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))
s.WriteString(t.getBorderStyle(0, i, BorderMiddleTop).Render(t.border.MiddleTop))
}
}
if t.borderRight {
s.WriteString(t.borderStyle.Render(t.border.TopRight))
s.WriteString(t.getBorderStyle(0, len(t.widths)-1, BorderTopRight).Render(t.border.TopRight))
}
return s.String()
}
Expand All @@ -411,16 +483,16 @@ func (t *Table) constructTopBorder() string {
func (t *Table) constructBottomBorder() string {
var s strings.Builder
if t.borderLeft {
s.WriteString(t.borderStyle.Render(t.border.BottomLeft))
s.WriteString(t.getBorderStyle(t.data.Rows(), 0, BorderBottomLeft).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])))
s.WriteString(t.getBorderStyle(t.data.Rows(), i, BorderBottom).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))
s.WriteString(t.getBorderStyle(t.data.Rows(), i, BorderMiddleBottom).Render(t.border.MiddleBottom))
}
}
if t.borderRight {
s.WriteString(t.borderStyle.Render(t.border.BottomRight))
s.WriteString(t.getBorderStyle(t.data.Rows(), len(t.widths)-1, BorderBottomRight).Render(t.border.BottomRight))
}
return s.String()
}
Expand All @@ -430,7 +502,7 @@ func (t *Table) constructBottomBorder() string {
func (t *Table) constructHeaders() string {
var s strings.Builder
if t.borderLeft {
s.WriteString(t.borderStyle.Render(t.border.Left))
s.WriteString(t.getBorderStyle(0, 0, BorderLeft).Render(t.border.Left))
}
for i, header := range t.headers {
s.WriteString(t.style(0, i).
Expand All @@ -439,29 +511,29 @@ func (t *Table) constructHeaders() string {
MaxWidth(t.widths[i]).
Render(runewidth.Truncate(header, t.widths[i], "…")))
if i < len(t.headers)-1 && t.borderColumn {
s.WriteString(t.borderStyle.Render(t.border.Left))
s.WriteString(t.getBorderStyle(0, i+1, BorderLeft).Render(t.border.Left))
}
}
if t.borderHeader {
if t.borderRight {
s.WriteString(t.borderStyle.Render(t.border.Right))
s.WriteString(t.getBorderStyle(0, len(t.headers)-1, BorderRight).Render(t.border.Right))
}
s.WriteString("\n")
if t.borderLeft {
s.WriteString(t.borderStyle.Render(t.border.MiddleLeft))
s.WriteString(t.getBorderStyle(0, 0, BorderMiddleLeft).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])))
s.WriteString(t.getBorderStyle(0, i, BorderBottom).Render(strings.Repeat(t.border.Bottom, t.widths[i])))
if i < len(t.headers)-1 && t.borderColumn {
s.WriteString(t.borderStyle.Render(t.border.Middle))
s.WriteString(t.getBorderStyle(0, i, BorderMiddle).Render(t.border.Middle))
}
}
if t.borderRight {
s.WriteString(t.borderStyle.Render(t.border.MiddleRight))
s.WriteString(t.getBorderStyle(0, len(t.headers)-1, BorderMiddleRight).Render(t.border.MiddleRight))
}
}
if t.borderRight && !t.borderHeader {
s.WriteString(t.borderStyle.Render(t.border.Right))
s.WriteString(t.getBorderStyle(0, len(t.headers)-1, BorderRight).Render(t.border.Right))
}
return s.String()
}
Expand All @@ -475,9 +547,8 @@ func (t *Table) constructRow(index int) string {
height := t.heights[index+btoi(hasHeaders)]

var cells []string
left := strings.Repeat(t.borderStyle.Render(t.border.Left)+"\n", height)
if t.borderLeft {
cells = append(cells, left)
cells = append(cells, strings.Repeat(t.getBorderStyle(index+1, 0, BorderLeft).Render(t.border.Left)+"\n", height))
}

for c := 0; c < t.data.Columns(); c++ {
Expand All @@ -491,12 +562,12 @@ func (t *Table) constructRow(index int) string {
Render(runewidth.Truncate(cell, t.widths[c]*height, "…")))

if c < t.data.Columns()-1 && t.borderColumn {
cells = append(cells, left)
cells = append(cells, strings.Repeat(t.getBorderStyle(index+1, c+1, BorderLeft).Render(t.border.Left)+"\n", height))
}
}

if t.borderRight {
right := strings.Repeat(t.borderStyle.Render(t.border.Right)+"\n", height)
right := strings.Repeat(t.getBorderStyle(index+1, t.data.Columns()-1, BorderRight).Render(t.border.Right)+"\n", height)
cells = append(cells, right)
}

Expand All @@ -507,14 +578,14 @@ func (t *Table) constructRow(index int) string {
s.WriteString(lipgloss.JoinHorizontal(lipgloss.Top, cells...) + "\n")

if t.borderRow && index < t.data.Rows()-1 {
s.WriteString(t.borderStyle.Render(t.border.MiddleLeft))
s.WriteString(t.getBorderStyle(index+1, 0, BorderMiddleLeft).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])))
s.WriteString(t.getBorderStyle(index+1, i, BorderBottom).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.getBorderStyle(index+1, i, BorderMiddle).Render(t.border.Middle))
}
}
s.WriteString(t.borderStyle.Render(t.border.MiddleRight) + "\n")
s.WriteString(t.getBorderStyle(index+1, len(t.widths)-1, BorderMiddleRight).Render(t.border.MiddleRight) + "\n")
}

return s.String()
Expand Down