From 2d8950e620c20521440f813b39dc48115061b6c9 Mon Sep 17 00:00:00 2001 From: Lonny Wong Date: Sat, 20 Jan 2024 00:00:09 +0800 Subject: [PATCH 1/2] feat(table): supports custom table border styles for each part --- examples/table/border/main.go | 68 ++++++++++++++++++++++++++ table/table.go | 89 +++++++++++++++++++++++++---------- 2 files changed, 131 insertions(+), 26 deletions(-) create mode 100644 examples/table/border/main.go diff --git a/examples/table/border/main.go b/examples/table/border/main.go new file mode 100644 index 00000000..04ed57a4 --- /dev/null +++ b/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() + } +} diff --git a/table/table.go b/table/table.go index 953721a0..9ad923ef 100644 --- a/table/table.go +++ b/table/table.go @@ -31,6 +31,29 @@ 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() @@ -38,8 +61,9 @@ func DefaultStyles(_, _ int) lipgloss.Style { // 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 @@ -102,6 +126,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 @@ -392,16 +430,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() } @@ -411,16 +449,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() } @@ -430,7 +468,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). @@ -439,29 +477,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() } @@ -475,9 +513,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++ { @@ -491,12 +528,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) } @@ -507,14 +544,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() From 9fbb23cafd3a3cedbb81a4ecff8559695938349a Mon Sep 17 00:00:00 2001 From: Lonny Wong Date: Fri, 26 Jan 2024 23:03:09 +0800 Subject: [PATCH 2/2] feat(table): supports fixed columns and exposes total width --- table/table.go | 44 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/table/table.go b/table/table.go index 9ad923ef..50017a76 100644 --- a/table/table.go +++ b/table/table.go @@ -86,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 @@ -328,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 @@ -403,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)