feat(table): respect border settings

This commit is contained in:
bashbunni
2025-04-02 14:37:51 -07:00
parent a0c4366a92
commit 738180eae6
+109 -95
View File
@@ -2,13 +2,14 @@
package table
import (
"reflect"
"strings"
"github.com/charmbracelet/bubbles/v2/help"
"github.com/charmbracelet/bubbles/v2/key"
"github.com/charmbracelet/lipgloss/v2/table"
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
"github.com/charmbracelet/lipgloss/v2/table"
)
// Model defines a state for the table widget.
@@ -16,11 +17,11 @@ type Model struct {
KeyMap KeyMap
Help help.Model
headers []string
rows [][]string
cursor int
focus bool
styles Styles
headers []string
rows [][]string
cursor int
focus bool
styles Styles
yOffset int
table *table.Table
@@ -90,44 +91,45 @@ func DefaultKeyMap() KeyMap {
}
}
// Styles contains style definitions for this list component. By default, these
// Styles contains style definitions for this table component. By default, these
// values are generated by DefaultStyles.
type Styles struct {
Border lipgloss.Border
BorderStyle lipgloss.Style
BorderTop bool
BorderBottom bool
BorderLeft bool
BorderRight bool
BorderColumn bool
BorderHeader bool
BorderRow bool
border lipgloss.Border
borderStyle lipgloss.Style
borderTop bool
borderBottom bool
borderLeft bool
borderRight bool
borderColumn bool
borderHeader bool
borderRow bool
Header lipgloss.Style
Cell lipgloss.Style
Selected lipgloss.Style
}
func DefaultStyles() Styles {
func DefaultStyles() Styles {
return Styles{
Header: lipgloss.NewStyle().Bold(true).Padding(0, 1),
Cell: lipgloss.NewStyle().Margin(0, 1),
Header: lipgloss.NewStyle().Bold(true).Padding(0, 1),
Cell: lipgloss.NewStyle().Margin(0, 1),
Selected: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("212")).Margin(0, 1),
}
}
}
func NewFromTemplate(t *table.Table, rows [][]string, headers []string) *Model {
m := &Model{
cursor: 0,
cursor: 0,
KeyMap: DefaultKeyMap(),
Help: help.New(),
table: t,
table: t,
}
m.SetRows(rows...)
m.SetHeaders(headers...)
return m
}
// SetBorder is a shorthand function for setting or unsetting borders on a
// table. The arguments work as follows:
//
@@ -152,7 +154,7 @@ func NewFromTemplate(t *table.Table, rows [][]string, headers []string) *Model {
//
// With more than four arguments nothing will be set.
func (m *Model) SetBorder(s ...bool) {
m.table.Border(m.styles.Border)
m.table.Border(m.styles.border)
top, right, bottom, left, rowSeparator, columnSeparator := m.whichSides(s...)
m.table.
BorderTop(top).
@@ -165,54 +167,63 @@ func (m *Model) SetBorder(s ...bool) {
// Border sets the top border.
func (m *Model) Border(border lipgloss.Border) *Model {
m.styles.border = border
m.table.Border(border)
return m
}
// BorderBottom sets the bottom border.
func (m *Model) BorderBottom(v bool) *Model {
m.styles.borderBottom = v
m.table.BorderBottom(v)
return m
}
// BorderTop sets the top border.
func (m *Model) BorderTop(v bool) *Model {
m.styles.borderTop = v
m.table.BorderTop(v)
return m
}
// BorderLeft sets the left border.
func (m *Model) BorderLeft(v bool) *Model {
m.styles.borderLeft = v
m.table.BorderLeft(v)
return m
}
// BorderRight sets the right border.
func (m *Model) BorderRight(v bool) *Model {
m.styles.borderRight = v
m.table.BorderRight(v)
return m
}
// BorderColumn sets the column border.
func (m *Model) BorderColumn(v bool) *Model {
m.styles.borderColumn = v
m.table.BorderColumn(v)
return m
}
// BorderHeader sets the header's border.
func (m *Model) BorderHeader(v bool) *Model {
m.styles.borderHeader = v
m.table.BorderHeader(v)
return m
}
// BorderRow sets the row borders.
func (m *Model) BorderRow(v bool) *Model {
m.styles.borderRow = v
m.table.BorderRow(v)
return m
}
// BorderStyle sets the style for the table border.
func (m *Model) BorderStyle(style lipgloss.Style) *Model {
m.styles.borderStyle = style
m.table.BorderStyle(style)
return m
}
@@ -228,8 +239,8 @@ func (m *Model) BorderStyle(style lipgloss.Style) *Model {
// 6: top -> right -> bottom -> left -> rowSeparator -> columnSeparator
func (m Model) whichSides(s ...bool) (top, right, bottom, left, rowSeparator, columnSeparator bool) {
// set the separators to true unless otherwise set.
rowSeparator = m.styles.BorderRow
columnSeparator = m.styles.BorderColumn
rowSeparator = m.styles.borderRow
columnSeparator = m.styles.borderColumn
switch len(s) {
case 1:
@@ -268,14 +279,13 @@ func (m Model) whichSides(s ...bool) (top, right, bottom, left, rowSeparator, co
rowSeparator = s[4]
columnSeparator = s[5]
default:
top = m.styles.BorderTop
right = m.styles.BorderRight
bottom = m.styles.BorderBottom
left = m.styles.BorderLeft
top = m.styles.borderTop
right = m.styles.borderRight
bottom = m.styles.borderBottom
left = m.styles.borderLeft
}
return top, right, bottom, left, rowSeparator, columnSeparator
}
}
// Option is used to set options in New. For example:
//
@@ -283,19 +293,31 @@ func (m Model) whichSides(s ...bool) (top, right, bottom, left, rowSeparator, co
type Option func(*Model)
// New creates a new model for the table widget.
func New(opts ...Option) Model {
func New(opts ...Option) *Model {
m := Model{
cursor: 0,
cursor: 0,
KeyMap: DefaultKeyMap(),
Help: help.New(),
styles: DefaultStyles(),
table: table.New(),
}
m.SetStyles(DefaultStyles())
// Set border defaults here...
m.Border(lipgloss.NormalBorder())
m.BorderTop(true)
m.BorderBottom(true)
m.BorderLeft(true)
m.BorderRight(true)
m.BorderColumn(false)
m.BorderRow(false)
m.BorderHeader(true)
for _, opt := range opts {
opt(&m)
}
return m
return &m
}
func (m *Model) SetHeaders(headers ...string) *Model {
@@ -304,11 +326,10 @@ func (m *Model) SetHeaders(headers ...string) *Model {
return m
}
// WithColumns sets the table columns (headers).
// WithHeaders sets the table headers.
func WithHeaders(headers ...string) Option {
return func(m *Model) {
m.headers = headers
m.table.Headers(headers...)
m.SetHeaders(headers...)
}
}
@@ -318,10 +339,10 @@ func (m *Model) SetRows(rows ...[]string) *Model {
return m
}
// WithRows sets the table rows (data).
// WithRows sets the table rows.
func WithRows(rows ...[]string) Option {
return func(m *Model) {
m.rows = rows
m.SetRows(rows...)
}
}
@@ -361,8 +382,21 @@ func WithFocused(f bool) Option {
}
}
// SetStyles sets the table styles.
func (m *Model) SetStyles(t *table.Table) {
// SetStyles sets the table styles.
func (m *Model) SetStyles(s Styles) {
// Update table styles.
if !reflect.DeepEqual(s.Selected, lipgloss.Style{}) {
m.styles.Selected = s.Selected
}
if !reflect.DeepEqual(s.Header, lipgloss.Style{}) {
m.styles.Header = s.Header
}
if !reflect.DeepEqual(s.Cell, lipgloss.Style{}) {
m.styles.Cell = s.Cell
}
}
func (m *Model) SetStyleFromLipgloss(t *table.Table) {
t.Rows(m.rows...)
t.Headers(m.headers...)
m.table = t
@@ -375,9 +409,9 @@ func (m *Model) SetStyleFunc(s table.StyleFunc) {
}
// WithStyles sets the table styles.
func WithStyles(t *table.Table) Option {
func WithStyles(s Styles) Option {
return func(m *Model) {
m.SetStyles(t)
m.SetStyles(s)
}
}
@@ -400,7 +434,9 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
if !m.focus {
return m, nil
}
height := len(m.rows)
table := m.table.String()
// TODO make this not hard coded?
height := lipgloss.Height(table) - 6
switch msg := msg.(type) {
case tea.KeyPressMsg:
@@ -445,6 +481,18 @@ func (m *Model) Blur() {
// View renders the component.
func (m Model) View() string {
// Update the position-sensitive styles as the cursor position may have
// changed in Update.
m.table.StyleFunc(func(row, col int) lipgloss.Style {
if row == m.cursor {
return m.styles.Selected
}
if row == table.HeaderRow {
return m.styles.Header
}
return m.styles.Cell
})
return m.table.String()
}
@@ -465,28 +513,36 @@ func (m Model) Headers() []string {
return m.headers
}
// WithWidth sets the width of the viewport of the table.
// WithWidth sets the width of the table.
func (m *Model) WithWidth(w int) {
m.table.Width(w)
}
// WithHeight sets the height of the viewport of the table.
// WithHeight sets the height of the table.
func (m *Model) WithHeight(h int) {
m.table.Height(h)
}
// TODO add docs to use lipgloss.Height and lipgloss.Width to get table height/width
// Cursor returns the index of the selected row.
func (m Model) Cursor() int {
return m.cursor
}
// WithCursor sets the cursor position in the table.
func (m *Model) WithCursor(n int) {
// SetCursor sets the cursor position in the table.
func (m *Model) SetCursor(n int) {
m.cursor = clamp(n, 0, len(m.rows)-1)
}
// SelectedRow returns the selected row. You can cast it to your own
// implementation.
func (m Model) SelectedRow() []string {
if m.cursor < 0 || m.cursor >= len(m.rows) {
return nil
}
return m.rows[m.cursor]
}
// SetYOffset sets the YOffset position in the table.
func (m *Model) SetYOffset(n int) {
m.yOffset = clamp(n, 0, len(m.rows)-1)
@@ -496,9 +552,7 @@ func (m *Model) SetYOffset(n int) {
// MoveUp moves the selection up by any number of rows.
// It can not go above the first row.
func (m *Model) MoveUp(n int) {
m.WithCursor(m.cursor - n)
// only set the offset outside of the last available rows.
m.SetCursor(m.cursor - n)
m.SetYOffset(m.yOffset - n)
m.table.YOffset(m.yOffset)
}
@@ -506,17 +560,11 @@ func (m *Model) MoveUp(n int) {
// MoveDown moves the selection down by any number of rows.
// It can not go below the last row.
func (m *Model) MoveDown(n int) {
// Once we're at the last set of rows, where there is no truncation
// stop setting the y offset and only move cursor
// Only move cursor on first and last pages
m.WithCursor(m.cursor + n)
// only set the offset outside of the last available rows.
m.SetCursor(m.cursor + n)
m.SetYOffset(m.yOffset + n)
m.table.YOffset(m.yOffset)
}
// GotoTop moves the selection to the first row.
func (m *Model) GotoTop() {
m.MoveUp(m.cursor)
@@ -541,40 +589,6 @@ func (m *Model) FromValues(value, separator string) {
m.SetRows(rows...)
}
// TODO remove this
// func (m Model) headersView() string {
// s := make([]string, 0, len(m.headers))
// for _, col := range m.headers {
// if col.Width <= 0 {
// continue
// }
// style := lipgloss.NewStyle().Width(col.Width).MaxWidth(col.Width).Inline(true)
// renderedCell := style.Render(runewidth.Truncate(col.Title, col.Width, "…"))
// s = append(s, m.styles.Header.Render(renderedCell))
// }
// return lipgloss.JoinHorizontal(lipgloss.Top, s...)
// }
// func (m *Model) renderRow(r int) string {
// s := make([]string, 0, len(m.headers))
// for i, value := range m.rows[r] {
// if m.headers[i].Width <= 0 {
// continue
// }
// style := lipgloss.NewStyle().Width(m.headers[i].Width).MaxWidth(m.headers[i].Width).Inline(true)
// renderedCell := m.styles.Cell.Render(style.Render(runewidth.Truncate(value, m.headers[i].Width, "…")))
// s = append(s, renderedCell)
// }
//
// row := lipgloss.JoinHorizontal(lipgloss.Top, s...)
//
// if r == m.cursor {
// return m.styles.Selected.Render(row)
// }
//
// return row
// }
func clamp(v, low, high int) int {
return min(max(v, low), high)
}