From a0c4366a923a220ab67cc10be58a40735ef45869 Mon Sep 17 00:00:00 2001 From: bashbunni Date: Tue, 1 Apr 2025 09:48:48 -0700 Subject: [PATCH 01/25] feat(table): use lipgloss table to render --- table/table.go | 490 +++++++++++++++++++++++++++++++------------------ 1 file changed, 311 insertions(+), 179 deletions(-) diff --git a/table/table.go b/table/table.go index 13241a128..150895452 100644 --- a/table/table.go +++ b/table/table.go @@ -6,10 +6,9 @@ import ( "github.com/charmbracelet/bubbles/v2/help" "github.com/charmbracelet/bubbles/v2/key" - "github.com/charmbracelet/bubbles/v2/viewport" + "github.com/charmbracelet/lipgloss/v2/table" tea "github.com/charmbracelet/bubbletea/v2" "github.com/charmbracelet/lipgloss/v2" - "github.com/mattn/go-runewidth" ) // Model defines a state for the table widget. @@ -17,24 +16,14 @@ type Model struct { KeyMap KeyMap Help help.Model - cols []Column - rows []Row + headers []string + rows [][]string cursor int focus bool styles Styles + yOffset int - viewport viewport.Model - start int - end int -} - -// Row represents one line in the table. -type Row []string - -// Column defines the table structure. -type Column struct { - Title string - Width int + table *table.Table } // KeyMap defines keybindings. It satisfies to the help.KeyMap interface, which @@ -104,26 +93,190 @@ func DefaultKeyMap() KeyMap { // Styles contains style definitions for this list 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 + Header lipgloss.Style Cell lipgloss.Style Selected lipgloss.Style } -// DefaultStyles returns a set of default style definitions for this table. -func DefaultStyles() Styles { + func DefaultStyles() Styles { return Styles{ - Selected: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("212")), - Header: lipgloss.NewStyle().Bold(true).Padding(0, 1), - Cell: lipgloss.NewStyle().Padding(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, + KeyMap: DefaultKeyMap(), + Help: help.New(), + 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: +// +// With one argument, the argument is applied to all sides. +// +// With two arguments, the arguments are applied to the vertical and horizontal +// sides, in that order. +// +// With three arguments, the arguments are applied to the top side, the +// horizontal sides, and the bottom side, in that order. +// +// With four arguments, the arguments are applied clockwise starting from the +// top side, followed by the right side, then the bottom, and finally the left. +// +// With five arguments, the arguments are applied clockwise starting from the +// top side, followed by the right side, then the bottom, and finally the left. +// The final value will set the row separator. +// +// With six arguments, the arguments are applied clockwise starting from the +// top side, followed by the right side, then the bottom, and finally the left. +// The final two values will set the row and column separators in that order. +// +// With more than four arguments nothing will be set. +func (m *Model) SetBorder(s ...bool) { + m.table.Border(m.styles.Border) + top, right, bottom, left, rowSeparator, columnSeparator := m.whichSides(s...) + m.table. + BorderTop(top). + BorderRight(right). + BorderBottom(bottom). + BorderLeft(left). + BorderRow(rowSeparator). + BorderColumn(columnSeparator) +} + +// Border sets the top border. +func (m *Model) Border(border lipgloss.Border) *Model { + m.table.Border(border) + return m +} + +// BorderBottom sets the bottom border. +func (m *Model) BorderBottom(v bool) *Model { + m.table.BorderBottom(v) + return m +} + +// BorderTop sets the top border. +func (m *Model) BorderTop(v bool) *Model { + m.table.BorderTop(v) + return m +} + +// BorderLeft sets the left border. +func (m *Model) BorderLeft(v bool) *Model { + m.table.BorderLeft(v) + return m +} + +// BorderRight sets the right border. +func (m *Model) BorderRight(v bool) *Model { + m.table.BorderRight(v) + return m +} + +// BorderColumn sets the column border. +func (m *Model) BorderColumn(v bool) *Model { + m.table.BorderColumn(v) + return m } -// SetStyles sets the table styles. -func (m *Model) SetStyles(s Styles) { - m.styles = s - m.UpdateViewport() +// BorderHeader sets the header's border. +func (m *Model) BorderHeader(v bool) *Model { + m.table.BorderHeader(v) + return m } +// BorderRow sets the row borders. +func (m *Model) BorderRow(v bool) *Model { + m.table.BorderRow(v) + return m +} + +// BorderStyle sets the style for the table border. +func (m *Model) BorderStyle(style lipgloss.Style) *Model { + m.table.BorderStyle(style) + return m +} + +// whichSides is a helper method for setting values on sides of a block based on +// the number of arguments given. +// 0: set all sides to true +// 1: set all sides to given arg +// 2: top -> bottom +// 3: top -> horizontal -> bottom +// 4: top -> right -> bottom -> left +// 5: top -> right -> bottom -> left -> rowSeparator +// 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 + + switch len(s) { + case 1: + top = s[0] + right = s[0] + bottom = s[0] + left = s[0] + rowSeparator = s[0] + columnSeparator = s[0] + case 2: + top = s[0] + right = s[1] + bottom = s[0] + left = s[1] + case 3: + top = s[0] + right = s[1] + bottom = s[2] + left = s[1] + case 4: + top = s[0] + right = s[1] + bottom = s[2] + left = s[3] + case 5: + top = s[0] + right = s[1] + bottom = s[2] + left = s[3] + rowSeparator = s[4] + case 6: + top = s[0] + right = s[1] + bottom = s[2] + left = s[3] + rowSeparator = s[4] + columnSeparator = s[5] + default: + 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: // // table := New(WithColumns([]Column{{Title: "ID", Width: 10}})) @@ -133,8 +286,6 @@ type Option func(*Model) func New(opts ...Option) Model { m := Model{ cursor: 0, - viewport: viewport.New(viewport.WithHeight(20)), //nolint:mnd - KeyMap: DefaultKeyMap(), Help: help.New(), styles: DefaultStyles(), @@ -144,39 +295,65 @@ func New(opts ...Option) Model { opt(&m) } - m.UpdateViewport() + return m +} +func (m *Model) SetHeaders(headers ...string) *Model { + m.headers = headers + m.table.Headers(headers...) return m } // WithColumns sets the table columns (headers). -func WithColumns(cols []Column) Option { +func WithHeaders(headers ...string) Option { return func(m *Model) { - m.cols = cols + m.headers = headers + m.table.Headers(headers...) } } +func (m *Model) SetRows(rows ...[]string) *Model { + m.rows = rows + m.table.Rows(rows...) + return m +} + // WithRows sets the table rows (data). -func WithRows(rows []Row) Option { +func WithRows(rows ...[]string) Option { return func(m *Model) { m.rows = rows } } +func (m *Model) SetHeight(h int) *Model { + m.table.Height(h) + return m +} + // WithHeight sets the height of the table. func WithHeight(h int) Option { return func(m *Model) { - m.viewport.SetHeight(h - lipgloss.Height(m.headersView())) + m.table.Height(h) } } +func (m *Model) SetWidth(w int) *Model { + m.table.Width(w) + return m +} + // WithWidth sets the width of the table. func WithWidth(w int) Option { return func(m *Model) { - m.viewport.SetWidth(w) + m.table.Width(w) } } +func (m *Model) SetFocused(f bool) *Model { + m.focus = f + return m +} + // WithFocused sets the focus state of the table. func WithFocused(f bool) Option { return func(m *Model) { @@ -184,10 +361,30 @@ func WithFocused(f bool) Option { } } + // SetStyles sets the table styles. +func (m *Model) SetStyles(t *table.Table) { + t.Rows(m.rows...) + t.Headers(m.headers...) + m.table = t +} + +// SetStyleFunc sets the table's custom StyleFunc. Use this for conditional +// styling e.g. styling a cell by its contents or by index. +func (m *Model) SetStyleFunc(s table.StyleFunc) { + m.table.StyleFunc(s) +} + // WithStyles sets the table styles. -func WithStyles(s Styles) Option { +func WithStyles(t *table.Table) Option { + return func(m *Model) { + m.SetStyles(t) + } +} + +// WithStyleFunc sets the table StyleFunc for conditional styling. +func WithStyleFunc(s table.StyleFunc) Option { return func(m *Model) { - m.styles = s + m.table.StyleFunc(s) } } @@ -203,6 +400,7 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { if !m.focus { return m, nil } + height := len(m.rows) switch msg := msg.(type) { case tea.KeyPressMsg: @@ -212,13 +410,13 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { case key.Matches(msg, m.KeyMap.LineDown): m.MoveDown(1) case key.Matches(msg, m.KeyMap.PageUp): - m.MoveUp(m.viewport.Height()) + m.MoveUp(height) case key.Matches(msg, m.KeyMap.PageDown): - m.MoveDown(m.viewport.Height()) + m.MoveDown(height) case key.Matches(msg, m.KeyMap.HalfPageUp): - m.MoveUp(m.viewport.Height() / 2) //nolint:mnd + m.MoveUp(height / 2) //nolint:mnd case key.Matches(msg, m.KeyMap.HalfPageDown): - m.MoveDown(m.viewport.Height() / 2) //nolint:mnd + m.MoveDown(height / 2) //nolint:mnd case key.Matches(msg, m.KeyMap.GotoTop): m.GotoTop() case key.Matches(msg, m.KeyMap.GotoBottom): @@ -238,18 +436,16 @@ func (m Model) Focused() bool { // interact. func (m *Model) Focus() { m.focus = true - m.UpdateViewport() } // Blur blurs the table, preventing selection or movement. func (m *Model) Blur() { m.focus = false - m.UpdateViewport() } // View renders the component. func (m Model) View() string { - return m.headersView() + "\n" + m.viewport.View() + return m.table.String() } // HelpView is a helper method for rendering the help menu from the keymap. @@ -259,131 +455,68 @@ func (m Model) HelpView() string { return m.Help.View(m.KeyMap) } -// UpdateViewport updates the list content based on the previously defined -// columns and rows. -func (m *Model) UpdateViewport() { - renderedRows := make([]string, 0, len(m.rows)) - - // Render only rows from: m.cursor-m.viewport.Height to: m.cursor+m.viewport.Height - // Constant runtime, independent of number of rows in a table. - // Limits the number of renderedRows to a maximum of 2*m.viewport.Height - if m.cursor >= 0 { - m.start = clamp(m.cursor-m.viewport.Height(), 0, m.cursor) - } else { - m.start = 0 - } - m.end = clamp(m.cursor+m.viewport.Height(), m.cursor, len(m.rows)) - for i := m.start; i < m.end; i++ { - renderedRows = append(renderedRows, m.renderRow(i)) - } - - m.viewport.SetContent( - lipgloss.JoinVertical(lipgloss.Left, renderedRows...), - ) -} - -// SelectedRow returns the selected row. -// You can cast it to your own implementation. -func (m Model) SelectedRow() Row { - if m.cursor < 0 || m.cursor >= len(m.rows) { - return nil - } - - return m.rows[m.cursor] -} - // Rows returns the current rows. -func (m Model) Rows() []Row { +func (m Model) Rows() [][]string { return m.rows } -// Columns returns the current columns. -func (m Model) Columns() []Column { - return m.cols -} - -// SetRows sets a new rows state. -func (m *Model) SetRows(r []Row) { - m.rows = r - m.UpdateViewport() -} - -// SetColumns sets a new columns state. -func (m *Model) SetColumns(c []Column) { - m.cols = c - m.UpdateViewport() -} - -// SetWidth sets the width of the viewport of the table. -func (m *Model) SetWidth(w int) { - m.viewport.SetWidth(w) - m.UpdateViewport() +// GetHeaders returns the current headers. +func (m Model) Headers() []string { + return m.headers } -// SetHeight sets the height of the viewport of the table. -func (m *Model) SetHeight(h int) { - m.viewport.SetHeight(h - lipgloss.Height(m.headersView())) - m.UpdateViewport() +// WithWidth sets the width of the viewport of the table. +func (m *Model) WithWidth(w int) { + m.table.Width(w) } -// Height returns the viewport height of the table. -func (m Model) Height() int { - return m.viewport.Height() +// WithHeight sets the height of the viewport of the table. +func (m *Model) WithHeight(h int) { + m.table.Height(h) } -// Width returns the viewport width of the table. -func (m Model) Width() int { - return m.viewport.Width() -} +// 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 } -// SetCursor sets the cursor position in the table. -func (m *Model) SetCursor(n int) { +// WithCursor sets the cursor position in the table. +func (m *Model) WithCursor(n int) { m.cursor = clamp(n, 0, len(m.rows)-1) - m.UpdateViewport() +} + +// SetYOffset sets the YOffset position in the table. +func (m *Model) SetYOffset(n int) { + m.yOffset = clamp(n, 0, len(m.rows)-1) + m.table.YOffset(m.yOffset) } // 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.cursor = clamp(m.cursor-n, 0, len(m.rows)-1) - - offset := m.viewport.YOffset() - switch { - case m.start == 0: - offset = clamp(offset, 0, m.cursor) - case m.start < m.viewport.Height(): - offset = clamp(clamp(offset+n, 0, m.cursor), 0, m.viewport.Height()) - case offset >= 1: - offset = clamp(offset+n, 1, m.viewport.Height()) - } - m.viewport.SetYOffset(offset) - m.UpdateViewport() + m.WithCursor(m.cursor - n) + + // only set the offset outside of the last available rows. + m.SetYOffset(m.yOffset - n) + m.table.YOffset(m.yOffset) } // MoveDown moves the selection down by any number of rows. // It can not go below the last row. func (m *Model) MoveDown(n int) { - m.cursor = clamp(m.cursor+n, 0, len(m.rows)-1) - m.UpdateViewport() - - offset := m.viewport.YOffset() - switch { - case m.end == len(m.rows) && offset > 0: - offset = clamp(offset-n, 1, m.viewport.Height()) - case m.cursor > (m.end-m.start)/2 && offset > 0: - offset = clamp(offset-n, 1, m.cursor) - case offset > 1: - case m.cursor > offset+m.viewport.Height()-1: - offset = clamp(offset+1, 0, 1) - } - m.viewport.SetYOffset(offset) + // 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.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) @@ -398,50 +531,49 @@ func (m *Model) GotoBottom() { // default for getting all the rows and the given separator for the fields on // each row. func (m *Model) FromValues(value, separator string) { - rows := []Row{} - for _, line := range strings.Split(value, "\n") { - r := Row{} - for _, field := range strings.Split(line, separator) { - r = append(r, field) + var rows [][]string + for i, line := range strings.Split(value, "\n") { + for j, field := range strings.Split(line, separator) { + rows[i][j] = field } - rows = append(rows, r) } - m.SetRows(rows) -} - -func (m Model) headersView() string { - s := make([]string, 0, len(m.cols)) - for _, col := range m.cols { - 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.cols)) - for i, value := range m.rows[r] { - if m.cols[i].Width <= 0 { - continue - } - style := lipgloss.NewStyle().Width(m.cols[i].Width).MaxWidth(m.cols[i].Width).Inline(true) - renderedCell := m.styles.Cell.Render(style.Render(runewidth.Truncate(value, m.cols[i].Width, "…"))) - s = append(s, renderedCell) - } - - row := lipgloss.JoinHorizontal(lipgloss.Top, s...) - - if r == m.cursor { - return m.styles.Selected.Render(row) - } - - return row -} + 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) From 738180eae6321159370bcf8966a4f95b623c208f Mon Sep 17 00:00:00 2001 From: bashbunni Date: Wed, 2 Apr 2025 14:37:51 -0700 Subject: [PATCH 02/25] feat(table): respect border settings --- table/table.go | 204 ++++++++++++++++++++++++++----------------------- 1 file changed, 109 insertions(+), 95 deletions(-) diff --git a/table/table.go b/table/table.go index 150895452..ea9fd1e78 100644 --- a/table/table.go +++ b/table/table.go @@ -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) } From a687f2fdc963e640a2c3775a760c2db14a623575 Mon Sep 17 00:00:00 2001 From: bashbunni Date: Thu, 3 Apr 2025 15:47:24 -0700 Subject: [PATCH 03/25] refactor: tidy + update godoc --- table/table.go | 409 +++++++++++++++++++++++++++---------------------- 1 file changed, 222 insertions(+), 187 deletions(-) diff --git a/table/table.go b/table/table.go index ea9fd1e78..caf247595 100644 --- a/table/table.go +++ b/table/table.go @@ -17,12 +17,13 @@ type Model struct { KeyMap KeyMap Help help.Model - headers []string - rows [][]string - cursor int - focus bool - styles Styles - yOffset int + headers []string + rows [][]string + cursor int + focus bool + styles Styles + yOffset int + useStyleFunc bool table *table.Table } @@ -153,7 +154,7 @@ func NewFromTemplate(t *table.Table, rows [][]string, headers []string) *Model { // The final two values will set the row and column separators in that order. // // With more than four arguments nothing will be set. -func (m *Model) SetBorder(s ...bool) { +func (m *Model) SetBorder(s ...bool) *Model { m.table.Border(m.styles.border) top, right, bottom, left, rowSeparator, columnSeparator := m.whichSides(s...) m.table. @@ -163,6 +164,7 @@ func (m *Model) SetBorder(s ...bool) { BorderLeft(left). BorderRow(rowSeparator). BorderColumn(columnSeparator) + return m } // Border sets the top border. @@ -228,163 +230,127 @@ func (m *Model) BorderStyle(style lipgloss.Style) *Model { return m } -// whichSides is a helper method for setting values on sides of a block based on -// the number of arguments given. -// 0: set all sides to true -// 1: set all sides to given arg -// 2: top -> bottom -// 3: top -> horizontal -> bottom -// 4: top -> right -> bottom -> left -// 5: top -> right -> bottom -> left -> rowSeparator -// 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 +// Options - switch len(s) { - case 1: - top = s[0] - right = s[0] - bottom = s[0] - left = s[0] - rowSeparator = s[0] - columnSeparator = s[0] - case 2: - top = s[0] - right = s[1] - bottom = s[0] - left = s[1] - case 3: - top = s[0] - right = s[1] - bottom = s[2] - left = s[1] - case 4: - top = s[0] - right = s[1] - bottom = s[2] - left = s[3] - case 5: - top = s[0] - right = s[1] - bottom = s[2] - left = s[3] - rowSeparator = s[4] - case 6: - top = s[0] - right = s[1] - bottom = s[2] - left = s[3] - rowSeparator = s[4] - columnSeparator = s[5] - default: - top = m.styles.borderTop - right = m.styles.borderRight - bottom = m.styles.borderBottom - left = m.styles.borderLeft +// Option is used to set options in [New]. For example: +// +// table := New(WithHeaders([]string{"Rank", "City", "Country", "Population"})) +type Option func(*Model) + +// WithHeaders sets the table headers. +func WithHeaders(headers ...string) Option { + return func(m *Model) { + m.SetHeaders(headers...) } - return top, right, bottom, left, rowSeparator, columnSeparator } -// Option is used to set options in New. For example: +// TODO andrey confirm this... I'm pretty sure that's how it's working now // -// table := New(WithColumns([]Column{{Title: "ID", Width: 10}})) -type Option func(*Model) +// WithHeight sets the height of the table. The given height will be the total +// table height including borders, margins, and padding. +func WithHeight(h int) Option { + return func(m *Model) { + m.table.Height(h) + } +} -// New creates a new model for the table widget. -func New(opts ...Option) *Model { - m := Model{ - cursor: 0, - KeyMap: DefaultKeyMap(), - Help: help.New(), - table: table.New(), +// WithWidth sets the width of the table. The given width will be the total +// table width including borders, margins, and padding. +func WithWidth(w int) Option { + return func(m *Model) { + m.table.Width(w) } +} - m.SetStyles(DefaultStyles()) +// WithRows sets the table rows. +func WithRows(rows ...[]string) Option { + return func(m *Model) { + m.SetRows(rows...) + } +} - // 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) +// WithFocused sets the focus state of the table. This function is used as an +// [Option] in when creating a table with [New]. +func WithFocused(f bool) Option { + return func(m *Model) { + m.focus = f + } +} - for _, opt := range opts { - opt(&m) +// WithStyles sets the table styles. +func WithStyles(s Styles) Option { + return func(m *Model) { + m.SetStyles(s) } +} - return &m +// WithStyleFunc sets the table [table.StyleFunc] for conditional styling. +func WithStyleFunc(s table.StyleFunc) Option { + return func(m *Model) { + m.useStyleFunc = true + m.table.StyleFunc(s) + } } +// WithKeyMap sets the [KeyMap]. +func WithKeyMap(km KeyMap) Option { + return func(m *Model) { + m.KeyMap = km + } +} + +// Setters + +// SetHeaders sets the table headers. func (m *Model) SetHeaders(headers ...string) *Model { m.headers = headers m.table.Headers(headers...) return m } -// WithHeaders sets the table headers. -func WithHeaders(headers ...string) Option { - return func(m *Model) { - m.SetHeaders(headers...) - } -} - +// SetRows sets the table rows. func (m *Model) SetRows(rows ...[]string) *Model { m.rows = rows m.table.Rows(rows...) return m } -// WithRows sets the table rows. -func WithRows(rows ...[]string) Option { - return func(m *Model) { - m.SetRows(rows...) - } +// SetCursor sets the cursor position in the table. +func (m *Model) SetCursor(n int) *Model { + m.cursor = clamp(n, 0, len(m.rows)-1) + return m } +// SetHeight sets the width of the table. The given height will be the total +// table height including borders, margins, and padding. func (m *Model) SetHeight(h int) *Model { m.table.Height(h) return m } -// WithHeight sets the height of the table. -func WithHeight(h int) Option { - return func(m *Model) { - m.table.Height(h) - } -} - +// SetWidth sets the width of the table. The given width will be the total +// table width including borders, margins, and padding. func (m *Model) SetWidth(w int) *Model { m.table.Width(w) return m } -// WithWidth sets the width of the table. -func WithWidth(w int) Option { - return func(m *Model) { - m.table.Width(w) - } -} - +// SetFocused sets the focus state of the table. func (m *Model) SetFocused(f bool) *Model { m.focus = f return m } -// WithFocused sets the focus state of the table. -func WithFocused(f bool) Option { - return func(m *Model) { - m.focus = f - } +// SetYOffset sets the YOffset position in the table. +func (m *Model) SetYOffset(n int) *Model { + m.yOffset = clamp(n, 0, len(m.rows)-1) + m.table.YOffset(m.yOffset) + return m } -// SetStyles sets the table styles. -func (m *Model) SetStyles(s Styles) { - // Update table styles. +// SetStyles sets the table styles, only applying non-empty [Styles]. Note: using +// [Model.SetStyleFunc] will override styles set in this function. +func (m *Model) SetStyles(s Styles) *Model { if !reflect.DeepEqual(s.Selected, lipgloss.Style{}) { m.styles.Selected = s.Selected } @@ -394,42 +360,80 @@ func (m *Model) SetStyles(s Styles) { if !reflect.DeepEqual(s.Cell, lipgloss.Style{}) { m.styles.Cell = s.Cell } + return m } -func (m *Model) SetStyleFromLipgloss(t *table.Table) { +// OverwriteStyles sets the table styles, overwriting all existing styles. Note: +// using [Model.SetStyleFunc] will override styles set in this function. +func (m *Model) OverwriteStyles(s Styles) *Model { + m.styles = s + return m +} + +// TODO +func (m *Model) OverwriteStylesFromLipgloss(t *table.Table) { t.Rows(m.rows...) t.Headers(m.headers...) m.table = t } -// SetStyleFunc sets the table's custom StyleFunc. Use this for conditional +// SetStyleFunc sets the table's custom [table.StyleFunc]. Use this for conditional // styling e.g. styling a cell by its contents or by index. -func (m *Model) SetStyleFunc(s table.StyleFunc) { +func (m *Model) SetStyleFunc(s table.StyleFunc) *Model { + m.useStyleFunc = true m.table.StyleFunc(s) + return m } -// WithStyles sets the table styles. -func WithStyles(s Styles) Option { - return func(m *Model) { - m.SetStyles(s) +// Creation + +// New creates a new model for the table widget. +func New(opts ...Option) *Model { + m := Model{ + cursor: 0, + KeyMap: DefaultKeyMap(), + Help: help.New(), + table: table.New(), } -} -// WithStyleFunc sets the table StyleFunc for conditional styling. -func WithStyleFunc(s table.StyleFunc) Option { - return func(m *Model) { - m.table.StyleFunc(s) + 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 } -// WithKeyMap sets the key map. -func WithKeyMap(km KeyMap) Option { - return func(m *Model) { - m.KeyMap = km +// FromValues create the table rows from a simple string. It uses `\n` by +// default for getting all the rows and the given separator for the fields on +// each row. +func (m *Model) FromValues(value, separator string) { + rows := [][]string{} + for _, line := range strings.Split(value, "\n") { + row := []string{} + for _, field := range strings.Split(line, separator) { + row = append(row, field) + } + rows = append(rows, row) } + + m.SetRows(rows...) } -// Update is the Bubble Tea update loop. +// Bubble Tea Methods + +// Update is the Bubble Tea [tea.Model] update loop. func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { if !m.focus { return m, nil @@ -463,11 +467,6 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { return m, nil } -// Focused returns the focus state of the table. -func (m Model) Focused() bool { - return m.focus -} - // Focus focuses the table, allowing the user to move around the rows and // interact. func (m *Model) Focus() { @@ -479,20 +478,21 @@ func (m *Model) Blur() { m.focus = false } -// View renders the component. +// View renders the table [Model]. 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 - }) - + if !m.useStyleFunc { + // 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() } @@ -503,6 +503,13 @@ func (m Model) HelpView() string { return m.Help.View(m.KeyMap) } +// Getters + +// Focused returns the focus state of the table. +func (m Model) Focused() bool { + return m.focus +} + // Rows returns the current rows. func (m Model) Rows() [][]string { return m.rows @@ -513,26 +520,11 @@ func (m Model) Headers() []string { return m.headers } -// WithWidth sets the width of the table. -func (m *Model) WithWidth(w int) { - m.table.Width(w) -} - -// WithHeight sets the height of the table. -func (m *Model) WithHeight(h int) { - m.table.Height(h) -} - // Cursor returns the index of the selected row. func (m Model) Cursor() int { return m.cursor } -// 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 { @@ -543,11 +535,7 @@ func (m Model) SelectedRow() []string { 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) - m.table.YOffset(m.yOffset) -} +// Movement // MoveUp moves the selection up by any number of rows. // It can not go above the first row. @@ -575,20 +563,67 @@ func (m *Model) GotoBottom() { m.MoveDown(len(m.rows)) } -// FromValues create the table rows from a simple string. It uses `\n` by -// default for getting all the rows and the given separator for the fields on -// each row. -func (m *Model) FromValues(value, separator string) { - var rows [][]string - for i, line := range strings.Split(value, "\n") { - for j, field := range strings.Split(line, separator) { - rows[i][j] = field - } - } - - m.SetRows(rows...) -} +// Helpers func clamp(v, low, high int) int { return min(max(v, low), high) } + +// whichSides is a helper method for setting values on sides of a block based on +// the number of arguments given. +// 0: set all sides to true +// 1: set all sides to given arg +// 2: top -> bottom +// 3: top -> horizontal -> bottom +// 4: top -> right -> bottom -> left +// 5: top -> right -> bottom -> left -> rowSeparator +// 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 + + switch len(s) { + case 1: + top = s[0] + right = s[0] + bottom = s[0] + left = s[0] + rowSeparator = s[0] + columnSeparator = s[0] + case 2: + top = s[0] + right = s[1] + bottom = s[0] + left = s[1] + case 3: + top = s[0] + right = s[1] + bottom = s[2] + left = s[1] + case 4: + top = s[0] + right = s[1] + bottom = s[2] + left = s[3] + case 5: + top = s[0] + right = s[1] + bottom = s[2] + left = s[3] + rowSeparator = s[4] + case 6: + top = s[0] + right = s[1] + bottom = s[2] + left = s[3] + rowSeparator = s[4] + columnSeparator = s[5] + default: + top = m.styles.borderTop + right = m.styles.borderRight + bottom = m.styles.borderBottom + left = m.styles.borderLeft + } + return top, right, bottom, left, rowSeparator, columnSeparator +} From ae7fb0d5924286c1244733b9e357c4441d7c6474 Mon Sep 17 00:00:00 2001 From: bashbunni Date: Thu, 3 Apr 2025 16:02:02 -0700 Subject: [PATCH 04/25] feat(table): use padding for default cell styles --- table/table.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/table/table.go b/table/table.go index caf247595..381544ab9 100644 --- a/table/table.go +++ b/table/table.go @@ -110,14 +110,17 @@ type Styles struct { Selected lipgloss.Style } +// DefaultStyles returns sensible default table styles. func DefaultStyles() Styles { return Styles{ 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), + Cell: lipgloss.NewStyle().Padding(0, 1), + Selected: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("212")).Padding(0, 1), } } +// TODO do we want this? +// NewFromTemplate lets you create a table [Model] from Lip Gloss' [table.Table]. func NewFromTemplate(t *table.Table, rows [][]string, headers []string) *Model { m := &Model{ cursor: 0, From 5191362e39e83128b8cf5684a88cc653a6c54be1 Mon Sep 17 00:00:00 2001 From: bashbunni Date: Thu, 3 Apr 2025 16:02:50 -0700 Subject: [PATCH 05/25] test(table): add style and border tests --- table/table_test.go | 378 +++++++++++++----- .../TestOverwriteStyles/clear_styles.golden | 10 + .../TestOverwriteStyles/new_styles.golden | 10 + .../invalid_number_of_arguments.golden | 10 + .../TestSetBorder/no_left_border.golden | 10 + .../TestSetBorder/no_top_border.golden | 10 + .../TestSetBorder/unset_all_borders.golden | 10 + .../vertical_borders_only.golden | 10 + .../Clear_styles_with_StyleFunc.golden | 10 + .../empty_styles;_do_nothing.golden | 10 + .../testdata/TestSetStyles/new_styles.golden | 10 + .../TestTableAlignment/No_border.golden | 9 +- .../TestTableAlignment/With_border.golden | 15 +- 13 files changed, 385 insertions(+), 117 deletions(-) create mode 100644 table/testdata/TestOverwriteStyles/clear_styles.golden create mode 100644 table/testdata/TestOverwriteStyles/new_styles.golden create mode 100644 table/testdata/TestSetBorder/invalid_number_of_arguments.golden create mode 100644 table/testdata/TestSetBorder/no_left_border.golden create mode 100644 table/testdata/TestSetBorder/no_top_border.golden create mode 100644 table/testdata/TestSetBorder/unset_all_borders.golden create mode 100644 table/testdata/TestSetBorder/vertical_borders_only.golden create mode 100644 table/testdata/TestSetStyleFunc/Clear_styles_with_StyleFunc.golden create mode 100644 table/testdata/TestSetStyles/empty_styles;_do_nothing.golden create mode 100644 table/testdata/TestSetStyles/new_styles.golden diff --git a/table/table_test.go b/table/table_test.go index cfe87807f..8cbdb9a2f 100644 --- a/table/table_test.go +++ b/table/table_test.go @@ -1,166 +1,336 @@ package table import ( - "strings" + "fmt" + "reflect" "testing" "github.com/charmbracelet/lipgloss/v2" - "github.com/charmbracelet/x/ansi" + "github.com/charmbracelet/lipgloss/v2/table" "github.com/charmbracelet/x/exp/golden" ) +// Reusable inputs + +var niceMargins = lipgloss.NewStyle().Padding(0, 1) +var headers = []string{"Rank", "City", "Country", "Population"} +var rows = [][]string{ + {"1", "Tokyo", "Japan", "37,274,000"}, + {"2", "Delhi", "India", "32,065,760"}, + {"3", "Shanghai", "China", "28,516,904"}, + {"4", "Dhaka", "Bangladesh", "22,478,116"}, + {"5", "São Paulo", "Brazil", "22,429,800"}, + {"6", "Mexico City", "Mexico", "22,085,140"}, + {"7", "Cairo", "Egypt", "21,750,020"}, + {"8", "Beijing", "China", "21,333,332"}, + {"9", "Mumbai", "India", "20,961,472"}, + {"10", "Osaka", "Japan", "19,059,856"}, + {"11", "Chongqing", "China", "16,874,740"}, + {"12", "Karachi", "Pakistan", "16,839,950"}, + {"13", "Istanbul", "Turkey", "15,636,243"}, + {"14", "Kinshasa", "DR Congo", "15,628,085"}, + {"15", "Lagos", "Nigeria", "15,387,639"}, + {"16", "Buenos Aires", "Argentina", "15,369,919"}, +} + +// Tests + +func TestNew(t *testing.T) { + headers := []string{"Rank", "City", "Country", "Population"} + rows := [][]string{ + {"1", "Tokyo", "Japan", "37,274,000"}, + {"2", "Delhi", "India", "32,065,760"}, + {"3", "Shanghai", "China", "28,516,904"}, + {"4", "Dhaka", "Bangladesh", "22,478,116"}, + {"5", "São Paulo", "Brazil", "22,429,800"}, + {"6", "Mexico City", "Mexico", "22,085,140"}, + {"7", "Cairo", "Egypt", "21,750,020"}, + {"8", "Beijing", "China", "21,333,332"}, + {"9", "Mumbai", "India", "20,961,472"}, + {"10", "Osaka", "Japan", "19,059,856"}, + } + t.Run("new with options", func(t *testing.T) { + tb := New( + WithHeaders(headers...), + WithRows(rows...), + WithHeight(10), + ) + tb.View() + }) + t.Run("new, no options", func(t *testing.T) { + tb := New().SetHeaders(headers...).SetRows(rows...) + tb.View() + }) +} + func TestFromValues(t *testing.T) { input := "foo1,bar1\nfoo2,bar2\nfoo3,bar3" - table := New(WithColumns([]Column{{Title: "Foo"}, {Title: "Bar"}})) + table := New(WithHeaders("Foo", "Bar")) table.FromValues(input, ",") if len(table.rows) != 3 { t.Fatalf("expect table to have 3 rows but it has %d", len(table.rows)) } - expect := []Row{ + expect := [][]string{ {"foo1", "bar1"}, {"foo2", "bar2"}, {"foo3", "bar3"}, } - if !deepEqual(table.rows, expect) { + if !reflect.DeepEqual(table.rows, expect) { t.Fatal("table rows is not equals to the input") } } func TestFromValuesWithTabSeparator(t *testing.T) { input := "foo1.\tbar1\nfoo,bar,baz\tbar,2" - table := New(WithColumns([]Column{{Title: "Foo"}, {Title: "Bar"}})) + table := New(WithHeaders("Foo", "Bar")) table.FromValues(input, "\t") if len(table.rows) != 2 { t.Fatalf("expect table to have 2 rows but it has %d", len(table.rows)) } - expect := []Row{ + expect := [][]string{ {"foo1.", "bar1"}, {"foo,bar,baz", "bar,2"}, } - if !deepEqual(table.rows, expect) { + if !reflect.DeepEqual(table.rows, expect) { t.Fatal("table rows is not equals to the input") } } -func deepEqual(a, b []Row) bool { - if len(a) != len(b) { - return false +func TestTableAlignment(t *testing.T) { + headers := []string{ + "Name", + "Country of Origin", + "Dunk-able", } - for i, r := range a { - for j, f := range r { - if f != b[i][j] { - return false - } - } + rows := [][]string{ + {"Chocolate Digestives", "UK", "Yes"}, + {"Tim Tams", "Australia", "No"}, + {"Hobnobs", "UK", "Yes"}, } - return true + t.Run("No border", func(t *testing.T) { + biscuits := New( + WithHeaders(headers...), + WithRows(rows...), + ). + // Remove default border. + SetBorder(false). + // Remove default border under header. + BorderHeader(false). + // Strip styles. + SetStyleFunc(func(_, _ int) lipgloss.Style { + return niceMargins + }) + golden.RequireEqual(t, []byte(biscuits.View())) + }) + t.Run("With border", func(t *testing.T) { + // TODO how do we style header border? + // s.Header = s.Header. + // BorderStyle(lipgloss.NormalBorder()). + // BorderForeground(lipgloss.Color("240")). + // Bold(false) + + biscuits := New( + WithHeaders(headers...), + WithRows(rows...), + ). + // Strip styles + SetStyleFunc(func(_, _ int) lipgloss.Style { + return niceMargins + }) + golden.RequireEqual(t, []byte(biscuits.View())) + }) } -var cols = []Column{ - {Title: "col1", Width: 10}, - {Title: "col2", Width: 10}, - {Title: "col3", Width: 10}, +// Test Styles + +func TestOverwriteStyles(t *testing.T) { + tests := []struct { + name string + styles Styles + }{ + + {"clear styles", Styles{ + Selected: lipgloss.NewStyle(), + Header: lipgloss.NewStyle(), + Cell: lipgloss.NewStyle(), + }}, + {"new styles", Styles{ + Selected: niceMargins.Foreground(lipgloss.Color("68")), + Header: niceMargins, + Cell: niceMargins, + }}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + tb := New( + WithHeaders(headers...), + WithRows(rows...), + WithFocused(true), + WithHeight(10), + ) + tb.OverwriteStyles(tc.styles) + golden.RequireEqual(t, []byte(tb.View())) + }) + } } -func TestRenderRow(t *testing.T) { +func TestSetStyles(t *testing.T) { tests := []struct { - name string - table *Model - expected string + name string + styles Styles }{ - { - name: "simple row", - table: &Model{ - rows: []Row{{"Foooooo", "Baaaaar", "Baaaaaz"}}, - cols: cols, - styles: Styles{Cell: lipgloss.NewStyle()}, - }, - expected: "Foooooo Baaaaar Baaaaaz ", - }, - { - name: "simple row with truncations", - table: &Model{ - rows: []Row{{"Foooooooooo", "Baaaaaaaaar", "Quuuuuuuuux"}}, - cols: cols, - styles: Styles{Cell: lipgloss.NewStyle()}, - }, - expected: "Foooooooo…Baaaaaaaa…Quuuuuuuu…", - }, - { - name: "simple row avoiding truncations", - table: &Model{ - rows: []Row{{"Fooooooooo", "Baaaaaaaar", "Quuuuuuuux"}}, - cols: cols, - styles: Styles{Cell: lipgloss.NewStyle()}, - }, - expected: "FoooooooooBaaaaaaaarQuuuuuuuux", - }, + {"empty styles; do nothing", Styles{ + Selected: lipgloss.NewStyle(), + Header: lipgloss.NewStyle(), + Cell: lipgloss.NewStyle(), + }}, + {"new styles", Styles{ + Selected: niceMargins.Background(lipgloss.Color("68")), + Header: niceMargins, + Cell: niceMargins, + }}, } + for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - row := tc.table.renderRow(0) - if row != tc.expected { - t.Fatalf("\n\nWant: \n%s\n\nGot: \n%s\n", tc.expected, row) - } + table := New( + WithHeaders(headers...), + WithRows(rows...), + WithFocused(true), + WithHeight(10), + ) + + table.SetStyles(tc.styles) + golden.RequireEqual(t, []byte(table.View())) }) } } -func TestTableAlignment(t *testing.T) { - t.Run("No border", func(t *testing.T) { - biscuits := New( - WithHeight(5), - WithColumns([]Column{ - {Title: "Name", Width: 25}, - {Title: "Country of Origin", Width: 16}, - {Title: "Dunk-able", Width: 12}, - }), - WithRows([]Row{ - {"Chocolate Digestives", "UK", "Yes"}, - {"Tim Tams", "Australia", "No"}, - {"Hobnobs", "UK", "Yes"}, - }), +func TestSetStyleFunc(t *testing.T) { + t.Run("Clear styles with StyleFunc", func(t *testing.T) { + tb := New( + WithHeaders(headers...), + WithRows(rows...), + WithFocused(true), + WithHeight(10), ) - got := ansiStrip(biscuits.View()) - golden.RequireEqual(t, []byte(got)) + tb.SetStyleFunc(table.StyleFunc(func(row, col int) lipgloss.Style { + if row == tb.Cursor() { + return niceMargins.Background(lipgloss.Color("68")) + } + return niceMargins + })) + golden.RequireEqual(t, []byte(tb.View())) }) - t.Run("With border", func(t *testing.T) { - baseStyle := lipgloss.NewStyle(). - BorderStyle(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color("240")) +} - s := DefaultStyles() - s.Header = s.Header. - BorderStyle(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color("240")). - BorderBottom(true). - Bold(false) +func TestSetBorder(t *testing.T) { + tests := []struct { + name string + borders []bool + }{ + {"unset all borders", []bool{false}}, + {"set all borders", []bool{true}}, + {"vertical borders only", []bool{true, false}}, + {"no top border", []bool{false, true, true}}, + {"no left border", []bool{true, true, true, false}}, + {"row separator and no right border", []bool{true, false, true, true, true}}, + {"set row and column separators", []bool{false, false, false, false, true, true}}, + {"invalid number of arguments", []bool{true, false, false, false, false, true, true}}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + tb := New( + WithHeaders(headers...), + WithRows(rows...), + WithFocused(true), + WithHeight(10), + ).SetBorder(tc.borders...) + golden.RequireEqual(t, []byte(tb.View())) + }) + } +} - biscuits := New( - WithHeight(5), - WithColumns([]Column{ - {Title: "Name", Width: 25}, - {Title: "Country of Origin", Width: 16}, - {Title: "Dunk-able", Width: 12}, - }), - WithRows([]Row{ - {"Chocolate Digestives", "UK", "Yes"}, - {"Tim Tams", "Australia", "No"}, - {"Hobnobs", "UK", "Yes"}, - }), - WithStyles(s), - ) - got := ansiStrip(baseStyle.Render(biscuits.View())) - golden.RequireEqual(t, []byte(got)) +// Examples + +func ExampleOption() { + var niceMargins = lipgloss.NewStyle().Padding(0, 1) + var headers = []string{"Rank", "City", "Country", "Population"} + var rows = [][]string{ + {"1", "Tokyo", "Japan", "37,274,000"}, + {"2", "Delhi", "India", "32,065,760"}, + {"3", "Shanghai", "China", "28,516,904"}, + {"4", "Dhaka", "Bangladesh", "22,478,116"}, + {"5", "São Paulo", "Brazil", "22,429,800"}, + {"6", "Mexico City", "Mexico", "22,085,140"}, + {"7", "Cairo", "Egypt", "21,750,020"}, + {"8", "Beijing", "China", "21,333,332"}, + {"9", "Mumbai", "India", "20,961,472"}, + {"10", "Osaka", "Japan", "19,059,856"}, + } + t := New( + WithHeaders(headers...), + WithRows(rows...), + WithFocused(true), + WithHeight(10), + ).OverwriteStyles(Styles{ + Selected: niceMargins, + Header: niceMargins, + Cell: niceMargins, }) -} + fmt.Println(t.View()) + // Output: + //┌───────────────────────────────────────────┐ + //│ Rank  City   Country   Population │ + //├───────────────────────────────────────────┤ + //│ 1   Tokyo   Japan   37,274,000 │ + //│ 2   Delhi   India   32,065,760 │ + //│ 3   Shanghai   China   28,516,904 │ + //│ 4   Dhaka   Bangladesh  22,478,116 │ + //│ 5   São Paulo   Brazil   22,429,800 │ + //│ …   …   …   …  │ + //└───────────────────────────────────────────┘ + } -func ansiStrip(s string) string { - // Replace all \r\n with \n - s = strings.ReplaceAll(s, "\r\n", "\n") - return ansi.Strip(s) +func ExampleModel_SetRows() { + var niceMargins = lipgloss.NewStyle().Padding(0, 1) + var headers = []string{"Rank", "City", "Country", "Population"} + var rows = [][]string{ + {"1", "Tokyo", "Japan", "37,274,000"}, + {"2", "Delhi", "India", "32,065,760"}, + {"3", "Shanghai", "China", "28,516,904"}, + {"4", "Dhaka", "Bangladesh", "22,478,116"}, + {"5", "São Paulo", "Brazil", "22,429,800"}, + {"6", "Mexico City", "Mexico", "22,085,140"}, + {"7", "Cairo", "Egypt", "21,750,020"}, + {"8", "Beijing", "China", "21,333,332"}, + {"9", "Mumbai", "India", "20,961,472"}, + {"10", "Osaka", "Japan", "19,059,856"}, + } + tb := New(). + SetHeaders(headers...). + SetRows(rows...). + SetHeight(10). + OverwriteStyles(Styles{ + Selected: niceMargins, + Header: niceMargins, + Cell: niceMargins, + }) + fmt.Println(tb.View()) + // Output: + //┌───────────────────────────────────────────┐ + //│ Rank  City   Country   Population │ + //├───────────────────────────────────────────┤ + //│ 1   Tokyo   Japan   37,274,000 │ + //│ 2   Delhi   India   32,065,760 │ + //│ 3   Shanghai   China   28,516,904 │ + //│ 4   Dhaka   Bangladesh  22,478,116 │ + //│ 5   São Paulo   Brazil   22,429,800 │ + //│ …   …   …   …  │ + //└───────────────────────────────────────────┘ } diff --git a/table/testdata/TestOverwriteStyles/clear_styles.golden b/table/testdata/TestOverwriteStyles/clear_styles.golden new file mode 100644 index 000000000..f70a09014 --- /dev/null +++ b/table/testdata/TestOverwriteStyles/clear_styles.golden @@ -0,0 +1,10 @@ +┌────────────────────────────────────┐ +│RankCity Country Population│ +├────────────────────────────────────┤ +│1 Tokyo Japan 37,274,000│ +│2 Delhi India 32,065,760│ +│3 Shanghai China 28,516,904│ +│4 Dhaka Bangladesh22,478,116│ +│5 São Paulo Brazil 22,429,800│ +│… … … … │ +└────────────────────────────────────┘ \ No newline at end of file diff --git a/table/testdata/TestOverwriteStyles/new_styles.golden b/table/testdata/TestOverwriteStyles/new_styles.golden new file mode 100644 index 000000000..3c3dfe81d --- /dev/null +++ b/table/testdata/TestOverwriteStyles/new_styles.golden @@ -0,0 +1,10 @@ +┌────────────────────────────────────────────┐ +│ Rank  City   Country   Population │ +├────────────────────────────────────────────┤ +│ 1   Tokyo   Japan   37,274,000 │ +│ 2   Delhi   India   32,065,760 │ +│ 3   Shanghai   China   28,516,904 │ +│ 4   Dhaka   Bangladesh  22,478,116 │ +│ 5   São Paulo   Brazil   22,429,800 │ +│ …   …   …   …  │ +└────────────────────────────────────────────┘ \ No newline at end of file diff --git a/table/testdata/TestSetBorder/invalid_number_of_arguments.golden b/table/testdata/TestSetBorder/invalid_number_of_arguments.golden new file mode 100644 index 000000000..06932cc5c --- /dev/null +++ b/table/testdata/TestSetBorder/invalid_number_of_arguments.golden @@ -0,0 +1,10 @@ +┌────────────────────────────────────────────┐ +│ Rank  City   Country   Population │ +├────────────────────────────────────────────┤ +│ 1   Tokyo   Japan   37,274,000 │ +│ 2   Delhi   India   32,065,760 │ +│ 3   Shanghai   China   28,516,904 │ +│ 4   Dhaka   Bangladesh  22,478,116 │ +│ 5   São Paulo   Brazil   22,429,800 │ +│ …   …   …   …  │ +└────────────────────────────────────────────┘ \ No newline at end of file diff --git a/table/testdata/TestSetBorder/no_left_border.golden b/table/testdata/TestSetBorder/no_left_border.golden new file mode 100644 index 000000000..ff3188702 --- /dev/null +++ b/table/testdata/TestSetBorder/no_left_border.golden @@ -0,0 +1,10 @@ +────────────────────────────────────────────┐ + Rank  City   Country   Population │ +────────────────────────────────────────────┤ + 1   Tokyo   Japan   37,274,000 │ + 2   Delhi   India   32,065,760 │ + 3   Shanghai   China   28,516,904 │ + 4   Dhaka   Bangladesh  22,478,116 │ + 5   São Paulo   Brazil   22,429,800 │ + …   …   …   …  │ +────────────────────────────────────────────┘ \ No newline at end of file diff --git a/table/testdata/TestSetBorder/no_top_border.golden b/table/testdata/TestSetBorder/no_top_border.golden new file mode 100644 index 000000000..07e3bd1e7 --- /dev/null +++ b/table/testdata/TestSetBorder/no_top_border.golden @@ -0,0 +1,10 @@ +│ Rank  City   Country   Population │ +├────────────────────────────────────────────┤ +│ 1   Tokyo   Japan   37,274,000 │ +│ 2   Delhi   India   32,065,760 │ +│ 3   Shanghai   China   28,516,904 │ +│ 4   Dhaka   Bangladesh  22,478,116 │ +│ 5   São Paulo   Brazil   22,429,800 │ +│ 6   Mexico City   Mexico   22,085,140 │ +│ …   …   …   …  │ +└────────────────────────────────────────────┘ \ No newline at end of file diff --git a/table/testdata/TestSetBorder/unset_all_borders.golden b/table/testdata/TestSetBorder/unset_all_borders.golden new file mode 100644 index 000000000..d86fb561a --- /dev/null +++ b/table/testdata/TestSetBorder/unset_all_borders.golden @@ -0,0 +1,10 @@ + Rank  City   Country   Population  +──────────────────────────────────────────── + 1   Tokyo   Japan   37,274,000  + 2   Delhi   India   32,065,760  + 3   Shanghai   China   28,516,904  + 4   Dhaka   Bangladesh  22,478,116  + 5   São Paulo   Brazil   22,429,800  + 6   Mexico City   Mexico   22,085,140  + …   …   …   …  + \ No newline at end of file diff --git a/table/testdata/TestSetBorder/vertical_borders_only.golden b/table/testdata/TestSetBorder/vertical_borders_only.golden new file mode 100644 index 000000000..ed68321fc --- /dev/null +++ b/table/testdata/TestSetBorder/vertical_borders_only.golden @@ -0,0 +1,10 @@ +──────────────────────────────────────────── + Rank  City   Country   Population  +──────────────────────────────────────────── + 1   Tokyo   Japan   37,274,000  + 2   Delhi   India   32,065,760  + 3   Shanghai   China   28,516,904  + 4   Dhaka   Bangladesh  22,478,116  + 5   São Paulo   Brazil   22,429,800  + …   …   …   …  +──────────────────────────────────────────── \ No newline at end of file diff --git a/table/testdata/TestSetStyleFunc/Clear_styles_with_StyleFunc.golden b/table/testdata/TestSetStyleFunc/Clear_styles_with_StyleFunc.golden new file mode 100644 index 000000000..175b9d532 --- /dev/null +++ b/table/testdata/TestSetStyleFunc/Clear_styles_with_StyleFunc.golden @@ -0,0 +1,10 @@ +┌────────────────────────────────────────────┐ +│ Rank  City   Country   Population │ +├────────────────────────────────────────────┤ +│ 1   Tokyo   Japan   37,274,000 │ +│ 2   Delhi   India   32,065,760 │ +│ 3   Shanghai   China   28,516,904 │ +│ 4   Dhaka   Bangladesh  22,478,116 │ +│ 5   São Paulo   Brazil   22,429,800 │ +│ …   …   …   …  │ +└────────────────────────────────────────────┘ \ No newline at end of file diff --git a/table/testdata/TestSetStyles/empty_styles;_do_nothing.golden b/table/testdata/TestSetStyles/empty_styles;_do_nothing.golden new file mode 100644 index 000000000..06932cc5c --- /dev/null +++ b/table/testdata/TestSetStyles/empty_styles;_do_nothing.golden @@ -0,0 +1,10 @@ +┌────────────────────────────────────────────┐ +│ Rank  City   Country   Population │ +├────────────────────────────────────────────┤ +│ 1   Tokyo   Japan   37,274,000 │ +│ 2   Delhi   India   32,065,760 │ +│ 3   Shanghai   China   28,516,904 │ +│ 4   Dhaka   Bangladesh  22,478,116 │ +│ 5   São Paulo   Brazil   22,429,800 │ +│ …   …   …   …  │ +└────────────────────────────────────────────┘ \ No newline at end of file diff --git a/table/testdata/TestSetStyles/new_styles.golden b/table/testdata/TestSetStyles/new_styles.golden new file mode 100644 index 000000000..175b9d532 --- /dev/null +++ b/table/testdata/TestSetStyles/new_styles.golden @@ -0,0 +1,10 @@ +┌────────────────────────────────────────────┐ +│ Rank  City   Country   Population │ +├────────────────────────────────────────────┤ +│ 1   Tokyo   Japan   37,274,000 │ +│ 2   Delhi   India   32,065,760 │ +│ 3   Shanghai   China   28,516,904 │ +│ 4   Dhaka   Bangladesh  22,478,116 │ +│ 5   São Paulo   Brazil   22,429,800 │ +│ …   …   …   …  │ +└────────────────────────────────────────────┘ \ No newline at end of file diff --git a/table/testdata/TestTableAlignment/No_border.golden b/table/testdata/TestTableAlignment/No_border.golden index 767f71b67..ed5c59c5f 100644 --- a/table/testdata/TestTableAlignment/No_border.golden +++ b/table/testdata/TestTableAlignment/No_border.golden @@ -1,5 +1,4 @@ - Name   Country of Orig…  Dunk-able   - Chocolate Digestives   UK   Yes   - Tim Tams   Australia   No   - Hobnobs   UK   Yes   - \ No newline at end of file + Name   Country of Origin  Dunk-able  + Chocolate Digestives  UK   Yes  + Tim Tams   Australia   No  + Hobnobs   UK   Yes  \ No newline at end of file diff --git a/table/testdata/TestTableAlignment/With_border.golden b/table/testdata/TestTableAlignment/With_border.golden index 64c906a47..f670d2005 100644 --- a/table/testdata/TestTableAlignment/With_border.golden +++ b/table/testdata/TestTableAlignment/With_border.golden @@ -1,8 +1,7 @@ -┌───────────────────────────────────────────────────────────┐ -│ Name   Country of Orig…  Dunk-able  │ -│───────────────────────────────────────────────────────────│ -│ Chocolate Digestives   UK   Yes  │ -│ Tim Tams   Australia   No  │ -│ Hobnobs   UK   Yes  │ -│ │ -└───────────────────────────────────────────────────────────┘ \ No newline at end of file +┌────────────────────────────────────────────────────┐ +│ Name   Country of Origin  Dunk-able │ +├────────────────────────────────────────────────────┤ +│ Chocolate Digestives  UK   Yes  │ +│ Tim Tams   Australia   No  │ +│ Hobnobs   UK   Yes  │ +└────────────────────────────────────────────────────┘ \ No newline at end of file From 242df040b8e1a041b9e14bfd0e098fc94b4e93c7 Mon Sep 17 00:00:00 2001 From: bashbunni Date: Thu, 3 Apr 2025 21:36:54 -0700 Subject: [PATCH 06/25] feat(table): create table bubble from a lip gloss table --- table/table.go | 18 ++-- table/table_test.go | 101 +++++++++++++++++++++- table/testdata/TestNewFromTemplate.golden | 32 +++++++ 3 files changed, 142 insertions(+), 9 deletions(-) create mode 100644 table/testdata/TestNewFromTemplate.golden diff --git a/table/table.go b/table/table.go index 381544ab9..3f84d467c 100644 --- a/table/table.go +++ b/table/table.go @@ -119,17 +119,19 @@ func DefaultStyles() Styles { } } -// TODO do we want this? // NewFromTemplate lets you create a table [Model] from Lip Gloss' [table.Table]. -func NewFromTemplate(t *table.Table, rows [][]string, headers []string) *Model { +func NewFromTemplate(t *table.Table, headers []string, rows [][]string) *Model { m := &Model{ - cursor: 0, - KeyMap: DefaultKeyMap(), - Help: help.New(), - table: t, + cursor: 0, + KeyMap: DefaultKeyMap(), + Help: help.New(), + table: t, + useStyleFunc: true, } - m.SetRows(rows...) - m.SetHeaders(headers...) + // We can't get the rows and headers from the table, so the user needs to + // provide them as arguments. + m.rows = rows + m.headers = headers return m } diff --git a/table/table_test.go b/table/table_test.go index 8cbdb9a2f..50455563b 100644 --- a/table/table_test.go +++ b/table/table_test.go @@ -2,6 +2,7 @@ package table import ( "fmt" + "image/color" "reflect" "testing" @@ -256,6 +257,104 @@ func TestSetBorder(t *testing.T) { } } +func TestNewFromTemplate(t *testing.T) { + // Using Pokemon example from https://github.com/charmbracelet/lipgloss. + baseStyle := lipgloss.NewStyle().Padding(0, 1) + headerStyle := baseStyle.Foreground(lipgloss.Color("252")).Bold(true) + selectedStyle := baseStyle.Foreground(lipgloss.Color("#01BE85")).Background(lipgloss.Color("#00432F")) + typeColors := map[string]color.Color{ + "Bug": lipgloss.Color("#D7FF87"), + "Electric": lipgloss.Color("#FDFF90"), + "Fire": lipgloss.Color("#FF7698"), + "Flying": lipgloss.Color("#FF87D7"), + "Grass": lipgloss.Color("#75FBAB"), + "Ground": lipgloss.Color("#FF875F"), + "Normal": lipgloss.Color("#929292"), + "Poison": lipgloss.Color("#7D5AFC"), + "Water": lipgloss.Color("#00E2C7"), + } + dimTypeColors := map[string]color.Color{ + "Bug": lipgloss.Color("#97AD64"), + "Electric": lipgloss.Color("#FCFF5F"), + "Fire": lipgloss.Color("#BA5F75"), + "Flying": lipgloss.Color("#C97AB2"), + "Grass": lipgloss.Color("#59B980"), + "Ground": lipgloss.Color("#C77252"), + "Normal": lipgloss.Color("#727272"), + "Poison": lipgloss.Color("#634BD0"), + "Water": lipgloss.Color("#439F8E"), + } + + pokemonHeaders := []string{"#", "Name", "Type 1", "Type 2", "Japanese", "Official Rom."} + pokemonData := [][]string{ + {"1", "Bulbasaur", "Grass", "Poison", "フシギダネ", "Fushigidane"}, + {"2", "Ivysaur", "Grass", "Poison", "フシギソウ", "Fushigisou"}, + {"3", "Venusaur", "Grass", "Poison", "フシギバナ", "Fushigibana"}, + {"4", "Charmander", "Fire", "", "ヒトカゲ", "Hitokage"}, + {"5", "Charmeleon", "Fire", "", "リザード", "Lizardo"}, + {"6", "Charizard", "Fire", "Flying", "リザードン", "Lizardon"}, + {"7", "Squirtle", "Water", "", "ゼニガメ", "Zenigame"}, + {"8", "Wartortle", "Water", "", "カメール", "Kameil"}, + {"9", "Blastoise", "Water", "", "カメックス", "Kamex"}, + {"10", "Caterpie", "Bug", "", "キャタピー", "Caterpie"}, + {"11", "Metapod", "Bug", "", "トランセル", "Trancell"}, + {"12", "Butterfree", "Bug", "Flying", "バタフリー", "Butterfree"}, + {"13", "Weedle", "Bug", "Poison", "ビードル", "Beedle"}, + {"14", "Kakuna", "Bug", "Poison", "コクーン", "Cocoon"}, + {"15", "Beedrill", "Bug", "Poison", "スピアー", "Spear"}, + {"16", "Pidgey", "Normal", "Flying", "ポッポ", "Poppo"}, + {"17", "Pidgeotto", "Normal", "Flying", "ピジョン", "Pigeon"}, + {"18", "Pidgeot", "Normal", "Flying", "ピジョット", "Pigeot"}, + {"19", "Rattata", "Normal", "", "コラッタ", "Koratta"}, + {"20", "Raticate", "Normal", "", "ラッタ", "Ratta"}, + {"21", "Spearow", "Normal", "Flying", "オニスズメ", "Onisuzume"}, + {"22", "Fearow", "Normal", "Flying", "オニドリル", "Onidrill"}, + {"23", "Ekans", "Poison", "", "アーボ", "Arbo"}, + {"24", "Arbok", "Poison", "", "アーボック", "Arbok"}, + {"25", "Pikachu", "Electric", "", "ピカチュウ", "Pikachu"}, + {"26", "Raichu", "Electric", "", "ライチュウ", "Raichu"}, + {"27", "Sandshrew", "Ground", "", "サンド", "Sand"}, + {"28", "Sandslash", "Ground", "", "サンドパン", "Sandpan"}, + } + + lipglossTable := table.New(). + Border(lipgloss.NormalBorder()). + BorderStyle(lipgloss.NewStyle().Foreground(lipgloss.Color("238"))). + Headers(pokemonHeaders...). + Width(80). + Rows(pokemonData...). + StyleFunc(func(row, col int) lipgloss.Style { + if row == table.HeaderRow { + return headerStyle + } + + if pokemonData[row][1] == "Pikachu" { + return selectedStyle + } + + even := row%2 == 0 + + switch col { + case 2, 3: // Type 1 + 2 + c := typeColors + if even { + c = dimTypeColors + } + + color := c[fmt.Sprint(pokemonData[row][col])] + return baseStyle.Foreground(color) + } + + if even { + return baseStyle.Foreground(lipgloss.Color("245")) + } + return baseStyle.Foreground(lipgloss.Color("252")) + }) + + bubblesTable := NewFromTemplate(lipglossTable, headers, pokemonData) + golden.RequireEqual(t, []byte(bubblesTable.View())) +} + // Examples func ExampleOption() { @@ -295,7 +394,7 @@ func ExampleOption() { //│ 5   São Paulo   Brazil   22,429,800 │ //│ …   …   …   …  │ //└───────────────────────────────────────────┘ - } +} func ExampleModel_SetRows() { var niceMargins = lipgloss.NewStyle().Padding(0, 1) diff --git a/table/testdata/TestNewFromTemplate.golden b/table/testdata/TestNewFromTemplate.golden new file mode 100644 index 000000000..b300d75e6 --- /dev/null +++ b/table/testdata/TestNewFromTemplate.golden @@ -0,0 +1,32 @@ +┌────────────┬────────────┬───────────┬───────────┬────────────┬───────────────┐ +│ #  │ Name  │ Type 1  │ Type 2  │ Japanese  │ Official Rom. │ +├────────────┼────────────┼───────────┼───────────┼────────────┼───────────────┤ +│ 1  │ Bulbasaur  │ Grass  │ Poison  │ フシギダネ │ Fushigidane  │ +│ 2  │ Ivysaur  │ Grass  │ Poison  │ フシギソウ │ Fushigisou  │ +│ 3  │ Venusaur  │ Grass  │ Poison  │ フシギバナ │ Fushigibana  │ +│ 4  │ Charmander │ Fire  │   │ ヒトカゲ  │ Hitokage  │ +│ 5  │ Charmeleon │ Fire  │   │ リザード  │ Lizardo  │ +│ 6  │ Charizard  │ Fire  │ Flying  │ リザードン │ Lizardon  │ +│ 7  │ Squirtle  │ Water  │   │ ゼニガメ  │ Zenigame  │ +│ 8  │ Wartortle  │ Water  │   │ カメール  │ Kameil  │ +│ 9  │ Blastoise  │ Water  │   │ カメックス │ Kamex  │ +│ 10  │ Caterpie  │ Bug  │   │ キャタピー │ Caterpie  │ +│ 11  │ Metapod  │ Bug  │   │ トランセル │ Trancell  │ +│ 12  │ Butterfree │ Bug  │ Flying  │ バタフリー │ Butterfree  │ +│ 13  │ Weedle  │ Bug  │ Poison  │ ビードル  │ Beedle  │ +│ 14  │ Kakuna  │ Bug  │ Poison  │ コクーン  │ Cocoon  │ +│ 15  │ Beedrill  │ Bug  │ Poison  │ スピアー  │ Spear  │ +│ 16  │ Pidgey  │ Normal  │ Flying  │ ポッポ  │ Poppo  │ +│ 17  │ Pidgeotto  │ Normal  │ Flying  │ ピジョン  │ Pigeon  │ +│ 18  │ Pidgeot  │ Normal  │ Flying  │ ピジョット │ Pigeot  │ +│ 19  │ Rattata  │ Normal  │   │ コラッタ  │ Koratta  │ +│ 20  │ Raticate  │ Normal  │   │ ラッタ  │ Ratta  │ +│ 21  │ Spearow  │ Normal  │ Flying  │ オニスズメ │ Onisuzume  │ +│ 22  │ Fearow  │ Normal  │ Flying  │ オニドリル │ Onidrill  │ +│ 23  │ Ekans  │ Poison  │   │ アーボ  │ Arbo  │ +│ 24  │ Arbok  │ Poison  │   │ アーボック │ Arbok  │ +│ 25  │ Pikachu  │ Electric  │   │ ピカチュウ │ Pikachu  │ +│ 26  │ Raichu  │ Electric  │   │ ライチュウ │ Raichu  │ +│ 27  │ Sandshrew  │ Ground  │   │ サンド  │ Sand  │ +│ 28  │ Sandslash  │ Ground  │   │ サンドパン │ Sandpan  │ +└────────────┴────────────┴───────────┴───────────┴────────────┴───────────────┘ \ No newline at end of file From 9489b3361383a46579bd8c276791bf19487598d3 Mon Sep 17 00:00:00 2001 From: bashbunni Date: Thu, 3 Apr 2025 22:15:35 -0700 Subject: [PATCH 07/25] docs(table): more godoc updates --- table/table.go | 54 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/table/table.go b/table/table.go index 3f84d467c..cd643ab0e 100644 --- a/table/table.go +++ b/table/table.go @@ -1,4 +1,4 @@ -// Package table provides a simple table component for Bubble Tea applications. +// Package table provides a table component for Bubble Tea applications. package table import ( @@ -92,8 +92,8 @@ func DefaultKeyMap() KeyMap { } } -// Styles contains style definitions for this table component. By default, these -// values are generated by DefaultStyles. +// Styles contains style definitions for this table component. Load default +// styles to your table with [DefaultStyles]. type Styles struct { border lipgloss.Border borderStyle lipgloss.Style @@ -119,7 +119,8 @@ func DefaultStyles() Styles { } } -// NewFromTemplate lets you create a table [Model] from Lip Gloss' [table.Table]. +// NewFromTemplate lets you create a table [Model] from Lip Gloss' +// [table.Table]. func NewFromTemplate(t *table.Table, headers []string, rows [][]string) *Model { m := &Model{ cursor: 0, @@ -158,7 +159,7 @@ func NewFromTemplate(t *table.Table, headers []string, rows [][]string) *Model { // top side, followed by the right side, then the bottom, and finally the left. // The final two values will set the row and column separators in that order. // -// With more than four arguments nothing will be set. +// With more than six arguments nothing will be set. func (m *Model) SetBorder(s ...bool) *Model { m.table.Border(m.styles.border) top, right, bottom, left, rowSeparator, columnSeparator := m.whichSides(s...) @@ -172,13 +173,20 @@ func (m *Model) SetBorder(s ...bool) *Model { return m } -// Border sets the top border. +// Border sets the kind of border to use for the table. See [lipgloss.Border]. func (m *Model) Border(border lipgloss.Border) *Model { m.styles.border = border m.table.Border(border) 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 +} + // BorderBottom sets the bottom border. func (m *Model) BorderBottom(v bool) *Model { m.styles.borderBottom = v @@ -214,7 +222,7 @@ func (m *Model) BorderColumn(v bool) *Model { return m } -// BorderHeader sets the header's border. +// BorderHeader sets the header border. func (m *Model) BorderHeader(v bool) *Model { m.styles.borderHeader = v m.table.BorderHeader(v) @@ -228,13 +236,6 @@ func (m *Model) BorderRow(v bool) *Model { 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 -} - // Options // Option is used to set options in [New]. For example: @@ -242,7 +243,8 @@ func (m *Model) BorderStyle(style lipgloss.Style) *Model { // table := New(WithHeaders([]string{"Rank", "City", "Country", "Population"})) type Option func(*Model) -// WithHeaders sets the table headers. +// WithHeaders sets the table headers. This function is used as an [Option] in +// when creating a table with [New]. func WithHeaders(headers ...string) Option { return func(m *Model) { m.SetHeaders(headers...) @@ -252,7 +254,8 @@ func WithHeaders(headers ...string) Option { // TODO andrey confirm this... I'm pretty sure that's how it's working now // // WithHeight sets the height of the table. The given height will be the total -// table height including borders, margins, and padding. +// table height including borders, margins, and padding. This function is used +// as an [Option] in when creating a table with [New]. func WithHeight(h int) Option { return func(m *Model) { m.table.Height(h) @@ -260,14 +263,16 @@ func WithHeight(h int) Option { } // WithWidth sets the width of the table. The given width will be the total -// table width including borders, margins, and padding. +// table width including borders, margins, and padding. This function is used as +// an [Option] in when creating a table with [New]. func WithWidth(w int) Option { return func(m *Model) { m.table.Width(w) } } -// WithRows sets the table rows. +// WithRows sets the table rows. This function is used as an [Option] in when +// creating a table with [New]. func WithRows(rows ...[]string) Option { return func(m *Model) { m.SetRows(rows...) @@ -282,14 +287,16 @@ func WithFocused(f bool) Option { } } -// WithStyles sets the table styles. +// WithStyles sets the table styles. This function is used as an [Option] in +// when creating a table with [New]. func WithStyles(s Styles) Option { return func(m *Model) { m.SetStyles(s) } } -// WithStyleFunc sets the table [table.StyleFunc] for conditional styling. +// WithStyleFunc sets the table [table.StyleFunc] for conditional styling. This +// function is used as an [Option] in when creating a table with [New]. func WithStyleFunc(s table.StyleFunc) Option { return func(m *Model) { m.useStyleFunc = true @@ -297,7 +304,8 @@ func WithStyleFunc(s table.StyleFunc) Option { } } -// WithKeyMap sets the [KeyMap]. +// WithKeyMap sets the [KeyMap]. This function is used as an [Option] in when +// creating a table with [New]. func WithKeyMap(km KeyMap) Option { return func(m *Model) { m.KeyMap = km @@ -375,11 +383,13 @@ func (m *Model) OverwriteStyles(s Styles) *Model { return m } -// TODO +// OverwriteStylesFromLipgloss sets the [Model]'s style attributes from an +// existing [lipgloss.Table]. func (m *Model) OverwriteStylesFromLipgloss(t *table.Table) { t.Rows(m.rows...) t.Headers(m.headers...) m.table = t + m.useStyleFunc = true } // SetStyleFunc sets the table's custom [table.StyleFunc]. Use this for conditional From 55b5472dcb41eef1479ece40410f0ebc08eb8f25 Mon Sep 17 00:00:00 2001 From: bashbunni Date: Thu, 3 Apr 2025 22:16:35 -0700 Subject: [PATCH 08/25] test(table): add TestOverwriteStylesFromLipgloss test --- table/table_test.go | 24 +++++++++++++++++++ .../TestOverwriteStylesFromLipgloss.golden | 20 ++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 table/testdata/TestOverwriteStylesFromLipgloss.golden diff --git a/table/table_test.go b/table/table_test.go index 50455563b..cf57f1b11 100644 --- a/table/table_test.go +++ b/table/table_test.go @@ -355,6 +355,30 @@ func TestNewFromTemplate(t *testing.T) { golden.RequireEqual(t, []byte(bubblesTable.View())) } +func TestOverwriteStylesFromLipgloss(t *testing.T) { + baseStyle := lipgloss.NewStyle().Padding(0, 1) + headerStyle := baseStyle.Foreground(lipgloss.Color("252")).Bold(true) + lipglossTable := table.New(). + Border(lipgloss.NormalBorder()). + BorderStyle(lipgloss.NewStyle().Foreground(lipgloss.Color("238"))). + Width(80). + StyleFunc(func(row, col int) lipgloss.Style { + if row == table.HeaderRow { + return headerStyle + } + + even := row%2 == 0 + + if even { + return baseStyle.Foreground(lipgloss.Color("245")) + } + return baseStyle.Foreground(lipgloss.Color("252")) + }) + bubblesTable := New().SetHeaders(headers...).SetRows(rows...) + bubblesTable.OverwriteStylesFromLipgloss(lipglossTable) + golden.RequireEqual(t, []byte(bubblesTable.View())) +} + // Examples func ExampleOption() { diff --git a/table/testdata/TestOverwriteStylesFromLipgloss.golden b/table/testdata/TestOverwriteStylesFromLipgloss.golden new file mode 100644 index 000000000..3cb3da3a3 --- /dev/null +++ b/table/testdata/TestOverwriteStylesFromLipgloss.golden @@ -0,0 +1,20 @@ +┌───────────────────┬───────────────────┬───────────────────┬──────────────────┐ +│ Rank  │ City  │ Country  │ Population  │ +├───────────────────┼───────────────────┼───────────────────┼──────────────────┤ +│ 1  │ Tokyo  │ Japan  │ 37,274,000  │ +│ 2  │ Delhi  │ India  │ 32,065,760  │ +│ 3  │ Shanghai  │ China  │ 28,516,904  │ +│ 4  │ Dhaka  │ Bangladesh  │ 22,478,116  │ +│ 5  │ São Paulo  │ Brazil  │ 22,429,800  │ +│ 6  │ Mexico City  │ Mexico  │ 22,085,140  │ +│ 7  │ Cairo  │ Egypt  │ 21,750,020  │ +│ 8  │ Beijing  │ China  │ 21,333,332  │ +│ 9  │ Mumbai  │ India  │ 20,961,472  │ +│ 10  │ Osaka  │ Japan  │ 19,059,856  │ +│ 11  │ Chongqing  │ China  │ 16,874,740  │ +│ 12  │ Karachi  │ Pakistan  │ 16,839,950  │ +│ 13  │ Istanbul  │ Turkey  │ 15,636,243  │ +│ 14  │ Kinshasa  │ DR Congo  │ 15,628,085  │ +│ 15  │ Lagos  │ Nigeria  │ 15,387,639  │ +│ 16  │ Buenos Aires  │ Argentina  │ 15,369,919  │ +└───────────────────┴───────────────────┴───────────────────┴──────────────────┘ \ No newline at end of file From 1360eaf0d3efddbad16ff5722b96b41cdf0126e7 Mon Sep 17 00:00:00 2001 From: bashbunni Date: Fri, 4 Apr 2025 09:28:56 -0700 Subject: [PATCH 09/25] fix(test): use valid naming for golden file --- table/table_test.go | 2 +- .../{empty_styles;_do_nothing.golden => empty_styles.golden} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename table/testdata/TestSetStyles/{empty_styles;_do_nothing.golden => empty_styles.golden} (100%) diff --git a/table/table_test.go b/table/table_test.go index cf57f1b11..49a84a191 100644 --- a/table/table_test.go +++ b/table/table_test.go @@ -185,7 +185,7 @@ func TestSetStyles(t *testing.T) { name string styles Styles }{ - {"empty styles; do nothing", Styles{ + {"empty styles", Styles{ Selected: lipgloss.NewStyle(), Header: lipgloss.NewStyle(), Cell: lipgloss.NewStyle(), diff --git a/table/testdata/TestSetStyles/empty_styles;_do_nothing.golden b/table/testdata/TestSetStyles/empty_styles.golden similarity index 100% rename from table/testdata/TestSetStyles/empty_styles;_do_nothing.golden rename to table/testdata/TestSetStyles/empty_styles.golden From 46da4a87cb97f7b9c0011c4ec3759ccbf6d43f49 Mon Sep 17 00:00:00 2001 From: bashbunni Date: Fri, 4 Apr 2025 10:08:40 -0700 Subject: [PATCH 10/25] feat(table): remove redundant SetFocused function --- table/table.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/table/table.go b/table/table.go index cd643ab0e..3e9ad7f82 100644 --- a/table/table.go +++ b/table/table.go @@ -348,12 +348,6 @@ func (m *Model) SetWidth(w int) *Model { return m } -// SetFocused sets the focus state of the table. -func (m *Model) SetFocused(f bool) *Model { - m.focus = f - return m -} - // SetYOffset sets the YOffset position in the table. func (m *Model) SetYOffset(n int) *Model { m.yOffset = clamp(n, 0, len(m.rows)-1) From a291578e286ba747b2207ca35a6a1a441fe83531 Mon Sep 17 00:00:00 2001 From: bashbunni Date: Fri, 4 Apr 2025 10:25:28 -0700 Subject: [PATCH 11/25] feat(table): remove FromValues feature --- table/table.go | 17 ----------------- table/table_test.go | 38 -------------------------------------- 2 files changed, 55 deletions(-) diff --git a/table/table.go b/table/table.go index 3e9ad7f82..51700f428 100644 --- a/table/table.go +++ b/table/table.go @@ -3,7 +3,6 @@ package table import ( "reflect" - "strings" "github.com/charmbracelet/bubbles/v2/help" "github.com/charmbracelet/bubbles/v2/key" @@ -424,22 +423,6 @@ func New(opts ...Option) *Model { return &m } -// FromValues create the table rows from a simple string. It uses `\n` by -// default for getting all the rows and the given separator for the fields on -// each row. -func (m *Model) FromValues(value, separator string) { - rows := [][]string{} - for _, line := range strings.Split(value, "\n") { - row := []string{} - for _, field := range strings.Split(line, separator) { - row = append(row, field) - } - rows = append(rows, row) - } - - m.SetRows(rows...) -} - // Bubble Tea Methods // Update is the Bubble Tea [tea.Model] update loop. diff --git a/table/table_test.go b/table/table_test.go index 49a84a191..9e9831229 100644 --- a/table/table_test.go +++ b/table/table_test.go @@ -3,7 +3,6 @@ package table import ( "fmt" "image/color" - "reflect" "testing" "github.com/charmbracelet/lipgloss/v2" @@ -64,43 +63,6 @@ func TestNew(t *testing.T) { }) } -func TestFromValues(t *testing.T) { - input := "foo1,bar1\nfoo2,bar2\nfoo3,bar3" - table := New(WithHeaders("Foo", "Bar")) - table.FromValues(input, ",") - - if len(table.rows) != 3 { - t.Fatalf("expect table to have 3 rows but it has %d", len(table.rows)) - } - - expect := [][]string{ - {"foo1", "bar1"}, - {"foo2", "bar2"}, - {"foo3", "bar3"}, - } - if !reflect.DeepEqual(table.rows, expect) { - t.Fatal("table rows is not equals to the input") - } -} - -func TestFromValuesWithTabSeparator(t *testing.T) { - input := "foo1.\tbar1\nfoo,bar,baz\tbar,2" - table := New(WithHeaders("Foo", "Bar")) - table.FromValues(input, "\t") - - if len(table.rows) != 2 { - t.Fatalf("expect table to have 2 rows but it has %d", len(table.rows)) - } - - expect := [][]string{ - {"foo1.", "bar1"}, - {"foo,bar,baz", "bar,2"}, - } - if !reflect.DeepEqual(table.rows, expect) { - t.Fatal("table rows is not equals to the input") - } -} - func TestTableAlignment(t *testing.T) { headers := []string{ "Name", From 7df895396bbd1ab042c3126fb08131355fbddb16 Mon Sep 17 00:00:00 2001 From: bashbunni Date: Fri, 4 Apr 2025 14:51:38 -0700 Subject: [PATCH 12/25] chore: tidy comments --- table/table.go | 2 -- table/table_test.go | 6 ------ 2 files changed, 8 deletions(-) diff --git a/table/table.go b/table/table.go index 51700f428..daea993fd 100644 --- a/table/table.go +++ b/table/table.go @@ -250,8 +250,6 @@ func WithHeaders(headers ...string) Option { } } -// TODO andrey confirm this... I'm pretty sure that's how it's working now -// // WithHeight sets the height of the table. The given height will be the total // table height including borders, margins, and padding. This function is used // as an [Option] in when creating a table with [New]. diff --git a/table/table_test.go b/table/table_test.go index 9e9831229..bd1ebdd0a 100644 --- a/table/table_test.go +++ b/table/table_test.go @@ -90,12 +90,6 @@ func TestTableAlignment(t *testing.T) { golden.RequireEqual(t, []byte(biscuits.View())) }) t.Run("With border", func(t *testing.T) { - // TODO how do we style header border? - // s.Header = s.Header. - // BorderStyle(lipgloss.NormalBorder()). - // BorderForeground(lipgloss.Color("240")). - // Bold(false) - biscuits := New( WithHeaders(headers...), WithRows(rows...), From 41f23fed580ff5fc2b40fbebd9bdb30f4dc6b5c0 Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Wed, 21 May 2025 14:39:05 -0300 Subject: [PATCH 13/25] test: review tests after merge. some yet to be addressed --- table/table_test.go | 90 ++++--------------- .../TestModel_View/Bordered_cells.golden | 21 ----- .../TestModel_View/Bordered_headers.golden | 23 ----- table/testdata/TestModel_View/Empty.golden | 20 ----- .../TestModel_View/Extra_padding.golden | 14 --- .../Manual_height_greater_than_rows.golden | 13 +-- .../Manual_height_less_than_rows.golden | 2 - .../Manual_width_greater_than_columns.golden | 28 ++---- .../Manual_width_less_than_columns.golden | 21 ----- .../Modified_viewport_height.golden | 3 - .../Multiple_rows_and_columns.golden | 28 ++---- .../testdata/TestModel_View/No_padding.golden | 17 ++-- .../Single_row_and_column.golden | 26 ++---- .../row_separator_and_no_right_border.golden | 16 ++++ 14 files changed, 65 insertions(+), 257 deletions(-) delete mode 100644 table/testdata/TestModel_View/Bordered_cells.golden delete mode 100644 table/testdata/TestModel_View/Bordered_headers.golden delete mode 100644 table/testdata/TestModel_View/Extra_padding.golden delete mode 100644 table/testdata/TestModel_View/Manual_height_less_than_rows.golden delete mode 100644 table/testdata/TestModel_View/Manual_width_less_than_columns.golden delete mode 100644 table/testdata/TestModel_View/Modified_viewport_height.golden create mode 100644 table/testdata/TestSetBorder/row_separator_and_no_right_border.golden diff --git a/table/table_test.go b/table/table_test.go index d0e1c26dc..7e86281e1 100644 --- a/table/table_test.go +++ b/table/table_test.go @@ -193,51 +193,6 @@ func TestOverwriteStyles(t *testing.T) { } } -func TestModel_RenderRow(t *testing.T) { - tests := []struct { - name string - table *Model - expected string - }{ - { - name: "simple row", - table: New( - WithRows([]string{"Foooooo", "Baaaaar", "Baaaaaz"}), - WithHeaders("col1", "col2", "col3"), - WithStyles(Styles{Cell: lipgloss.NewStyle()}), - ), - expected: "Foooooo Baaaaar Baaaaaz ", - }, - { - name: "simple row with truncations", - table: New( - WithRows([]string{"Foooooooooo", "Baaaaaaaaar", "Quuuuuuuuux"}), - WithHeaders("col1", "col2", "col3"), - WithStyles(Styles{Cell: lipgloss.NewStyle()}), - ), - expected: "Foooooooo…Baaaaaaaa…Quuuuuuuu…", - }, - { - name: "simple row avoiding truncations", - table: New( - WithRows([]string{"Fooooooooo", "Baaaaaaaar", "Quuuuuuuux"}), - WithHeaders("col1", "col2", "col3"), - WithStyles(Styles{Cell: lipgloss.NewStyle()}), - ), - expected: "FoooooooooBaaaaaaaarQuuuuuuuux", - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - actual := tc.table.table.Render() - if actual != tc.expected { - t.Fatalf("\n\nWant: \n%s\n\nGot: \n%s\n", tc.expected, actual) - } - }) - } -} - func TestSetStyles(t *testing.T) { tests := []struct { name string @@ -294,12 +249,12 @@ func TestSetBorder(t *testing.T) { borders []bool }{ {"unset all borders", []bool{false}}, - {"set all borders", []bool{true}}, + // {"set all borders", []bool{true}}, // FIXME(@andreynering): Fix on Lip Gloss. Unneeded extra row. {"vertical borders only", []bool{true, false}}, {"no top border", []bool{false, true, true}}, {"no left border", []bool{true, true, true, false}}, {"row separator and no right border", []bool{true, false, true, true, true}}, - {"set row and column separators", []bool{false, false, false, false, true, true}}, + // {"set row and column separators", []bool{false, false, false, false, true, true}}, // FIXME(@andreynering): Broken style, does this make sense? {"invalid number of arguments", []bool{true, false, false, false, false, true, true}}, } for _, tc := range tests { @@ -655,13 +610,13 @@ func TestModel_SetRows(t *testing.T) { t.Fatalf("want 0, got %d", len(table.rows)) } - table.SetRows([]string{"r1", "r2"}) + table.SetRows([]string{"r1"}, []string{"r2"}) if len(table.rows) != 2 { t.Fatalf("want 2, got %d", len(table.rows)) } - want := []string{"r1", "r2"} + want := [][]string{{"r1"}, {"r2"}} if !reflect.DeepEqual(table.rows, want) { t.Fatalf("\n\nwant %v\n\ngot %v", want, table.rows) } @@ -691,7 +646,6 @@ func TestModel_View(t *testing.T) { modelFunc func() *Model skip bool }{ - // TODO(?): should the view/output of empty tables use the same default height? (this has height 21) "Empty": { modelFunc: func() *Model { return New() @@ -719,7 +673,7 @@ func TestModel_View(t *testing.T) { ) }, }, - // TODO(fix): since the table height is tied to the viewport height, adding vertical padding to the headers' height directly increases the table height. + // FIXME(@andreynering): Fix this scenario in Lip Gloss "Extra padding": { modelFunc: func() *Model { s := DefaultStyles() @@ -737,6 +691,7 @@ func TestModel_View(t *testing.T) { WithStyles(s), ) }, + skip: true, }, "No padding": { modelFunc: func() *Model { @@ -756,7 +711,7 @@ func TestModel_View(t *testing.T) { ) }, }, - // TODO(?): the total height is modified with borderd headers, however not with bordered cells. Is this expected/desired? + // FIXME(@andreynering): Fix this scenario in Lip Gloss "Bordered headers": { modelFunc: func() *Model { return New( @@ -771,8 +726,9 @@ func TestModel_View(t *testing.T) { }), ) }, + skip: true, }, - // TODO(fix): Headers are not horizontally aligned with cells due to the border adding width to the cells. + // FIXME(@andreynering): Fix this scenario in Lip Gloss "Bordered cells": { modelFunc: func() *Model { return New( @@ -787,11 +743,13 @@ func TestModel_View(t *testing.T) { }), ) }, + skip: true, }, + // FIXME(@andreynering): Fix in Lip Gloss? Potentially add extra empty lines to the bottom of the table. "Manual height greater than rows": { modelFunc: func() *Model { return New( - WithHeight(6), + WithHeight(15), WithHeaders("Name", "Country of Origin", "Dunk-able"), WithRows( []string{"Chocolate Digestives", "UK", "Yes"}, @@ -801,6 +759,7 @@ func TestModel_View(t *testing.T) { ) }, }, + // FIXME(@andreynering): Fix this scenario in Lip Gloss. Should truncate table if height is too small. "Manual height less than rows": { modelFunc: func() *Model { return New( @@ -813,8 +772,8 @@ func TestModel_View(t *testing.T) { ), ) }, + skip: true, }, - // TODO(fix): spaces are added to the right of the viewport to fill the width, but the headers end as though they are not aware of the width. "Manual width greater than columns": { modelFunc: func() *Model { return New( @@ -828,8 +787,7 @@ func TestModel_View(t *testing.T) { ) }, }, - // TODO(fix): Setting the table width does not affect the total headers' width. Cells are wrapped. - // Headers are not affected. Truncation/resizing should match lipgloss.table functionality. + // FIXME(@andreynering): Fix this scenario in Lip Gloss. "Manual width less than columns": { modelFunc: func() *Model { return New( @@ -844,20 +802,6 @@ func TestModel_View(t *testing.T) { }, skip: true, }, - "Modified viewport height": { - modelFunc: func() *Model { - m := New( - WithHeaders("Name", "Country of Origin", "Dunk-able"), - WithRows( - []string{"Chocolate Digestives", "UK", "Yes"}, - []string{"Tim Tams", "Australia", "No"}, - []string{"Hobnobs", "UK", "Yes"}, - ), - ) - - return m - }, - }, } for name, tc := range tests { @@ -868,9 +812,7 @@ func TestModel_View(t *testing.T) { table := tc.modelFunc() - got := ansi.Strip(table.View()) - - golden.RequireEqual(t, []byte(got)) + golden.RequireEqual(t, []byte(table.View())) }) } } diff --git a/table/testdata/TestModel_View/Bordered_cells.golden b/table/testdata/TestModel_View/Bordered_cells.golden deleted file mode 100644 index 71e95988b..000000000 --- a/table/testdata/TestModel_View/Bordered_cells.golden +++ /dev/null @@ -1,21 +0,0 @@ -Name Country of Orig…Dunk-able -┌─────────────────────────┐┌────────────────┐┌────────────┐ -│Chocolate Digestives ││UK ││Yes │ -└─────────────────────────┘└────────────────┘└────────────┘ -┌─────────────────────────┐┌────────────────┐┌────────────┐ -│Tim Tams ││Australia ││No │ -└─────────────────────────┘└────────────────┘└────────────┘ -┌─────────────────────────┐┌────────────────┐┌────────────┐ -│Hobnobs ││UK ││Yes │ -└─────────────────────────┘└────────────────┘└────────────┘ - - - - - - - - - - - \ No newline at end of file diff --git a/table/testdata/TestModel_View/Bordered_headers.golden b/table/testdata/TestModel_View/Bordered_headers.golden deleted file mode 100644 index 0e260bae2..000000000 --- a/table/testdata/TestModel_View/Bordered_headers.golden +++ /dev/null @@ -1,23 +0,0 @@ -┌─────────────────────────┐┌────────────────┐┌────────────┐ -│Name ││Country of Orig…││Dunk-able │ -└─────────────────────────┘└────────────────┘└────────────┘ -Chocolate Digestives UK Yes -Tim Tams Australia No -Hobnobs UK Yes - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/table/testdata/TestModel_View/Empty.golden b/table/testdata/TestModel_View/Empty.golden index 7b050800f..e69de29bb 100644 --- a/table/testdata/TestModel_View/Empty.golden +++ b/table/testdata/TestModel_View/Empty.golden @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/table/testdata/TestModel_View/Extra_padding.golden b/table/testdata/TestModel_View/Extra_padding.golden deleted file mode 100644 index d6f6b76e8..000000000 --- a/table/testdata/TestModel_View/Extra_padding.golden +++ /dev/null @@ -1,14 +0,0 @@ - - -  Name     Country of Orig…    Dunk-able    - - - - -  Chocolate Digestives     UK     Yes    - - - - -  Tim Tams     Australia     No    - \ No newline at end of file diff --git a/table/testdata/TestModel_View/Manual_height_greater_than_rows.golden b/table/testdata/TestModel_View/Manual_height_greater_than_rows.golden index f89de1df9..747e8ee46 100644 --- a/table/testdata/TestModel_View/Manual_height_greater_than_rows.golden +++ b/table/testdata/TestModel_View/Manual_height_greater_than_rows.golden @@ -1,6 +1,7 @@ - Name   Country of Orig…  Dunk-able   - Chocolate Digestives   UK   Yes   - Tim Tams   Australia   No   - Hobnobs   UK   Yes   - - \ No newline at end of file +┌────────────────────────────────────────────────────┐ +│ Name   Country of Origin  Dunk-able │ +├────────────────────────────────────────────────────┤ +│ Chocolate Digestives  UK   Yes  │ +│ Tim Tams   Australia   No  │ +│ Hobnobs   UK   Yes  │ +└────────────────────────────────────────────────────┘ \ No newline at end of file diff --git a/table/testdata/TestModel_View/Manual_height_less_than_rows.golden b/table/testdata/TestModel_View/Manual_height_less_than_rows.golden deleted file mode 100644 index 83bded11d..000000000 --- a/table/testdata/TestModel_View/Manual_height_less_than_rows.golden +++ /dev/null @@ -1,2 +0,0 @@ - Name   Country of Orig…  Dunk-able   - Chocolate Digestives   UK   Yes   \ No newline at end of file diff --git a/table/testdata/TestModel_View/Manual_width_greater_than_columns.golden b/table/testdata/TestModel_View/Manual_width_greater_than_columns.golden index a1c06fee1..fc0f8211c 100644 --- a/table/testdata/TestModel_View/Manual_width_greater_than_columns.golden +++ b/table/testdata/TestModel_View/Manual_width_greater_than_columns.golden @@ -1,21 +1,7 @@ - Name   Country of Orig…  Dunk-able   - Chocolate Digestives   UK   Yes   - Tim Tams   Australia   No   - Hobnobs   UK   Yes   - - - - - - - - - - - - - - - - - \ No newline at end of file +┌─────────────────────────────────────────────────────────────────────────────── +│ Name   Country of Origin   Dunk-able  +├─────────────────────────────────────────────────────────────────────────────── +│ Chocolate Digestives   UK   Yes  +│ Tim Tams   Australia   No  +│ Hobnobs   UK   Yes  +└─────────────────────────────────────────────────────────────────────────────── \ No newline at end of file diff --git a/table/testdata/TestModel_View/Manual_width_less_than_columns.golden b/table/testdata/TestModel_View/Manual_width_less_than_columns.golden deleted file mode 100644 index d2bc25b96..000000000 --- a/table/testdata/TestModel_View/Manual_width_less_than_columns.golden +++ /dev/null @@ -1,21 +0,0 @@ - Name Country of Origin Dunk-able - Chocolate Digestives UK Yes - Tim Tams Australia No - Hobnobs UK Yes - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/table/testdata/TestModel_View/Modified_viewport_height.golden b/table/testdata/TestModel_View/Modified_viewport_height.golden deleted file mode 100644 index 98a44eeb9..000000000 --- a/table/testdata/TestModel_View/Modified_viewport_height.golden +++ /dev/null @@ -1,3 +0,0 @@ - Name   Country of Orig…  Dunk-able   - Chocolate Digestives   UK   Yes   - Tim Tams   Australia   No   \ No newline at end of file diff --git a/table/testdata/TestModel_View/Multiple_rows_and_columns.golden b/table/testdata/TestModel_View/Multiple_rows_and_columns.golden index d1dffc5ba..747e8ee46 100644 --- a/table/testdata/TestModel_View/Multiple_rows_and_columns.golden +++ b/table/testdata/TestModel_View/Multiple_rows_and_columns.golden @@ -1,21 +1,7 @@ - Name   Country of Orig…  Dunk-able   - Chocolate Digestives   UK   Yes   - Tim Tams   Australia   No   - Hobnobs   UK   Yes   - - - - - - - - - - - - - - - - - \ No newline at end of file +┌────────────────────────────────────────────────────┐ +│ Name   Country of Origin  Dunk-able │ +├────────────────────────────────────────────────────┤ +│ Chocolate Digestives  UK   Yes  │ +│ Tim Tams   Australia   No  │ +│ Hobnobs   UK   Yes  │ +└────────────────────────────────────────────────────┘ \ No newline at end of file diff --git a/table/testdata/TestModel_View/No_padding.golden b/table/testdata/TestModel_View/No_padding.golden index f74874668..747e8ee46 100644 --- a/table/testdata/TestModel_View/No_padding.golden +++ b/table/testdata/TestModel_View/No_padding.golden @@ -1,10 +1,7 @@ -Name Country of Orig…Dunk-able -Chocolate Digestives UK Yes -Tim Tams Australia No -Hobnobs UK Yes - - - - - - \ No newline at end of file +┌────────────────────────────────────────────────────┐ +│ Name   Country of Origin  Dunk-able │ +├────────────────────────────────────────────────────┤ +│ Chocolate Digestives  UK   Yes  │ +│ Tim Tams   Australia   No  │ +│ Hobnobs   UK   Yes  │ +└────────────────────────────────────────────────────┘ \ No newline at end of file diff --git a/table/testdata/TestModel_View/Single_row_and_column.golden b/table/testdata/TestModel_View/Single_row_and_column.golden index 36d5110eb..908a00cce 100644 --- a/table/testdata/TestModel_View/Single_row_and_column.golden +++ b/table/testdata/TestModel_View/Single_row_and_column.golden @@ -1,21 +1,5 @@ - Name   - Chocolate Digestives   - - - - - - - - - - - - - - - - - - - \ No newline at end of file +┌──────────────────────┐ +│ Name  │ +├──────────────────────┤ +│ Chocolate Digestives │ +└──────────────────────┘ \ No newline at end of file diff --git a/table/testdata/TestSetBorder/row_separator_and_no_right_border.golden b/table/testdata/TestSetBorder/row_separator_and_no_right_border.golden new file mode 100644 index 000000000..3fcc62996 --- /dev/null +++ b/table/testdata/TestSetBorder/row_separator_and_no_right_border.golden @@ -0,0 +1,16 @@ +┌──────────────────────────────────────────── +│ Rank  City   Country   Population  +├──────────────────────────────────────────── +│ 1   Tokyo   Japan   37,274,000  +├────────────────────────────────────────────┤ +│ 2   Delhi   India   32,065,760  +├────────────────────────────────────────────┤ +│ 3   Shanghai   China   28,516,904  +├────────────────────────────────────────────┤ +│ 4   Dhaka   Bangladesh  22,478,116  +├────────────────────────────────────────────┤ +│ 5   São Paulo   Brazil   22,429,800  +├────────────────────────────────────────────┤ +│ …   …   …   …  +├────────────────────────────────────────────┤ +└──────────────────────────────────────────── \ No newline at end of file From 8ee7e4bfda8ba271683fa03eff40af0d46dcaf04 Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Wed, 21 May 2025 17:59:27 -0300 Subject: [PATCH 14/25] refactor(table): rename tests to match a convention --- table/table_test.go | 50 +++++++++---------- ...den => ManualHeightGreaterThanRows.golden} | 0 ...n => ManualWidthGreaterThanColumns.golden} | 0 ...s.golden => MultipleRowsAndColumns.golden} | 0 .../{No_padding.golden => NoPadding.golden} | 0 ...olumn.golden => SingleRowAndColumn.golden} | 0 ...clear_styles.golden => ClearStyles.golden} | 0 .../{new_styles.golden => NewStyles.golden} | 0 ...golden => InvalidNumberOfArguments.golden} | 0 ...left_border.golden => NoLeftBorder.golden} | 0 ...o_top_border.golden => NoTopBorder.golden} | 0 ...en => RowSeparatorAndNoRightBorder.golden} | 0 ..._borders.golden => UnsetAllBorders.golden} | 0 ...only.golden => VerticalBordersOnly.golden} | 0 ...golden => ClearStylesWithStyleFunc.golden} | 0 ...empty_styles.golden => EmptyStyles.golden} | 0 .../{new_styles.golden => NewStyles.golden} | 0 .../{No_border.golden => NoBorder.golden} | 0 .../{With_border.golden => WithBorder.golden} | 0 19 files changed, 25 insertions(+), 25 deletions(-) rename table/testdata/TestModel_View/{Manual_height_greater_than_rows.golden => ManualHeightGreaterThanRows.golden} (100%) rename table/testdata/TestModel_View/{Manual_width_greater_than_columns.golden => ManualWidthGreaterThanColumns.golden} (100%) rename table/testdata/TestModel_View/{Multiple_rows_and_columns.golden => MultipleRowsAndColumns.golden} (100%) rename table/testdata/TestModel_View/{No_padding.golden => NoPadding.golden} (100%) rename table/testdata/TestModel_View/{Single_row_and_column.golden => SingleRowAndColumn.golden} (100%) rename table/testdata/TestOverwriteStyles/{clear_styles.golden => ClearStyles.golden} (100%) rename table/testdata/TestOverwriteStyles/{new_styles.golden => NewStyles.golden} (100%) rename table/testdata/TestSetBorder/{invalid_number_of_arguments.golden => InvalidNumberOfArguments.golden} (100%) rename table/testdata/TestSetBorder/{no_left_border.golden => NoLeftBorder.golden} (100%) rename table/testdata/TestSetBorder/{no_top_border.golden => NoTopBorder.golden} (100%) rename table/testdata/TestSetBorder/{row_separator_and_no_right_border.golden => RowSeparatorAndNoRightBorder.golden} (100%) rename table/testdata/TestSetBorder/{unset_all_borders.golden => UnsetAllBorders.golden} (100%) rename table/testdata/TestSetBorder/{vertical_borders_only.golden => VerticalBordersOnly.golden} (100%) rename table/testdata/TestSetStyleFunc/{Clear_styles_with_StyleFunc.golden => ClearStylesWithStyleFunc.golden} (100%) rename table/testdata/TestSetStyles/{empty_styles.golden => EmptyStyles.golden} (100%) rename table/testdata/TestSetStyles/{new_styles.golden => NewStyles.golden} (100%) rename table/testdata/TestTableAlignment/{No_border.golden => NoBorder.golden} (100%) rename table/testdata/TestTableAlignment/{With_border.golden => WithBorder.golden} (100%) diff --git a/table/table_test.go b/table/table_test.go index 7e86281e1..2358c693d 100644 --- a/table/table_test.go +++ b/table/table_test.go @@ -133,7 +133,7 @@ func TestTableAlignment(t *testing.T) { {"Tim Tams", "Australia", "No"}, {"Hobnobs", "UK", "Yes"}, } - t.Run("No border", func(t *testing.T) { + t.Run("NoBorder", func(t *testing.T) { biscuits := New( WithHeaders(headers...), WithRows(rows...), @@ -148,7 +148,7 @@ func TestTableAlignment(t *testing.T) { }) golden.RequireEqual(t, []byte(biscuits.View())) }) - t.Run("With border", func(t *testing.T) { + t.Run("WithBorder", func(t *testing.T) { biscuits := New( WithHeaders(headers...), WithRows(rows...), @@ -168,12 +168,12 @@ func TestOverwriteStyles(t *testing.T) { name string styles Styles }{ - {"clear styles", Styles{ + {"ClearStyles", Styles{ Selected: lipgloss.NewStyle(), Header: lipgloss.NewStyle(), Cell: lipgloss.NewStyle(), }}, - {"new styles", Styles{ + {"NewStyles", Styles{ Selected: niceMargins.Foreground(lipgloss.Color("68")), Header: niceMargins, Cell: niceMargins, @@ -198,12 +198,12 @@ func TestSetStyles(t *testing.T) { name string styles Styles }{ - {"empty styles", Styles{ + {"EmptyStyles", Styles{ Selected: lipgloss.NewStyle(), Header: lipgloss.NewStyle(), Cell: lipgloss.NewStyle(), }}, - {"new styles", Styles{ + {"NewStyles", Styles{ Selected: niceMargins.Background(lipgloss.Color("68")), Header: niceMargins, Cell: niceMargins, @@ -226,7 +226,7 @@ func TestSetStyles(t *testing.T) { } func TestSetStyleFunc(t *testing.T) { - t.Run("Clear styles with StyleFunc", func(t *testing.T) { + t.Run("ClearStylesWithStyleFunc", func(t *testing.T) { tb := New( WithHeaders(headers...), WithRows(rows...), @@ -248,14 +248,14 @@ func TestSetBorder(t *testing.T) { name string borders []bool }{ - {"unset all borders", []bool{false}}, - // {"set all borders", []bool{true}}, // FIXME(@andreynering): Fix on Lip Gloss. Unneeded extra row. - {"vertical borders only", []bool{true, false}}, - {"no top border", []bool{false, true, true}}, - {"no left border", []bool{true, true, true, false}}, - {"row separator and no right border", []bool{true, false, true, true, true}}, - // {"set row and column separators", []bool{false, false, false, false, true, true}}, // FIXME(@andreynering): Broken style, does this make sense? - {"invalid number of arguments", []bool{true, false, false, false, false, true, true}}, + {"UnsetAllBorders", []bool{false}}, + //{"SetAllBorders", []bool{true}}, // FIXME(@andreynering): Fix on Lip Gloss. Unneeded extra row. + {"VerticalBordersOnly", []bool{true, false}}, + {"NoTopBorder", []bool{false, true, true}}, + {"NoLeftBorder", []bool{true, true, true, false}}, + {"RowSeparatorAndNoRightBorder", []bool{true, false, true, true, true}}, + //{"SetRowAndColumnSeparators", []bool{false, false, false, false, true, true}}, // FIXME(@andreynering): Broken style, does this make sense? + {"InvalidNumberOfArguments", []bool{true, false, false, false, false, true, true}}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { @@ -651,7 +651,7 @@ func TestModel_View(t *testing.T) { return New() }, }, - "Single row and column": { + "SingleRowAndColumn": { modelFunc: func() *Model { return New( WithHeaders("Name"), @@ -661,7 +661,7 @@ func TestModel_View(t *testing.T) { ) }, }, - "Multiple rows and columns": { + "MultipleRowsAndColumns": { modelFunc: func() *Model { return New( WithHeaders("Name", "Country of Origin", "Dunk-able"), @@ -674,7 +674,7 @@ func TestModel_View(t *testing.T) { }, }, // FIXME(@andreynering): Fix this scenario in Lip Gloss - "Extra padding": { + "ExtraPadding": { modelFunc: func() *Model { s := DefaultStyles() s.Header = lipgloss.NewStyle().Padding(2, 2) @@ -693,7 +693,7 @@ func TestModel_View(t *testing.T) { }, skip: true, }, - "No padding": { + "NoPadding": { modelFunc: func() *Model { s := DefaultStyles() s.Header = lipgloss.NewStyle() @@ -712,7 +712,7 @@ func TestModel_View(t *testing.T) { }, }, // FIXME(@andreynering): Fix this scenario in Lip Gloss - "Bordered headers": { + "BorderedHeaders": { modelFunc: func() *Model { return New( WithHeaders("Name", "Country of Origin", "Dunk-able"), @@ -729,7 +729,7 @@ func TestModel_View(t *testing.T) { skip: true, }, // FIXME(@andreynering): Fix this scenario in Lip Gloss - "Bordered cells": { + "BorderedCells": { modelFunc: func() *Model { return New( WithHeaders("Name", "Country of Origin", "Dunk-able"), @@ -746,7 +746,7 @@ func TestModel_View(t *testing.T) { skip: true, }, // FIXME(@andreynering): Fix in Lip Gloss? Potentially add extra empty lines to the bottom of the table. - "Manual height greater than rows": { + "ManualHeightGreaterThanRows": { modelFunc: func() *Model { return New( WithHeight(15), @@ -760,7 +760,7 @@ func TestModel_View(t *testing.T) { }, }, // FIXME(@andreynering): Fix this scenario in Lip Gloss. Should truncate table if height is too small. - "Manual height less than rows": { + "ManualHeightLessThanRows": { modelFunc: func() *Model { return New( WithHeight(2), @@ -774,7 +774,7 @@ func TestModel_View(t *testing.T) { }, skip: true, }, - "Manual width greater than columns": { + "ManualWidthGreaterThanColumns": { modelFunc: func() *Model { return New( WithWidth(80), @@ -788,7 +788,7 @@ func TestModel_View(t *testing.T) { }, }, // FIXME(@andreynering): Fix this scenario in Lip Gloss. - "Manual width less than columns": { + "ManualWidthLessThanColumns": { modelFunc: func() *Model { return New( WithWidth(30), diff --git a/table/testdata/TestModel_View/Manual_height_greater_than_rows.golden b/table/testdata/TestModel_View/ManualHeightGreaterThanRows.golden similarity index 100% rename from table/testdata/TestModel_View/Manual_height_greater_than_rows.golden rename to table/testdata/TestModel_View/ManualHeightGreaterThanRows.golden diff --git a/table/testdata/TestModel_View/Manual_width_greater_than_columns.golden b/table/testdata/TestModel_View/ManualWidthGreaterThanColumns.golden similarity index 100% rename from table/testdata/TestModel_View/Manual_width_greater_than_columns.golden rename to table/testdata/TestModel_View/ManualWidthGreaterThanColumns.golden diff --git a/table/testdata/TestModel_View/Multiple_rows_and_columns.golden b/table/testdata/TestModel_View/MultipleRowsAndColumns.golden similarity index 100% rename from table/testdata/TestModel_View/Multiple_rows_and_columns.golden rename to table/testdata/TestModel_View/MultipleRowsAndColumns.golden diff --git a/table/testdata/TestModel_View/No_padding.golden b/table/testdata/TestModel_View/NoPadding.golden similarity index 100% rename from table/testdata/TestModel_View/No_padding.golden rename to table/testdata/TestModel_View/NoPadding.golden diff --git a/table/testdata/TestModel_View/Single_row_and_column.golden b/table/testdata/TestModel_View/SingleRowAndColumn.golden similarity index 100% rename from table/testdata/TestModel_View/Single_row_and_column.golden rename to table/testdata/TestModel_View/SingleRowAndColumn.golden diff --git a/table/testdata/TestOverwriteStyles/clear_styles.golden b/table/testdata/TestOverwriteStyles/ClearStyles.golden similarity index 100% rename from table/testdata/TestOverwriteStyles/clear_styles.golden rename to table/testdata/TestOverwriteStyles/ClearStyles.golden diff --git a/table/testdata/TestOverwriteStyles/new_styles.golden b/table/testdata/TestOverwriteStyles/NewStyles.golden similarity index 100% rename from table/testdata/TestOverwriteStyles/new_styles.golden rename to table/testdata/TestOverwriteStyles/NewStyles.golden diff --git a/table/testdata/TestSetBorder/invalid_number_of_arguments.golden b/table/testdata/TestSetBorder/InvalidNumberOfArguments.golden similarity index 100% rename from table/testdata/TestSetBorder/invalid_number_of_arguments.golden rename to table/testdata/TestSetBorder/InvalidNumberOfArguments.golden diff --git a/table/testdata/TestSetBorder/no_left_border.golden b/table/testdata/TestSetBorder/NoLeftBorder.golden similarity index 100% rename from table/testdata/TestSetBorder/no_left_border.golden rename to table/testdata/TestSetBorder/NoLeftBorder.golden diff --git a/table/testdata/TestSetBorder/no_top_border.golden b/table/testdata/TestSetBorder/NoTopBorder.golden similarity index 100% rename from table/testdata/TestSetBorder/no_top_border.golden rename to table/testdata/TestSetBorder/NoTopBorder.golden diff --git a/table/testdata/TestSetBorder/row_separator_and_no_right_border.golden b/table/testdata/TestSetBorder/RowSeparatorAndNoRightBorder.golden similarity index 100% rename from table/testdata/TestSetBorder/row_separator_and_no_right_border.golden rename to table/testdata/TestSetBorder/RowSeparatorAndNoRightBorder.golden diff --git a/table/testdata/TestSetBorder/unset_all_borders.golden b/table/testdata/TestSetBorder/UnsetAllBorders.golden similarity index 100% rename from table/testdata/TestSetBorder/unset_all_borders.golden rename to table/testdata/TestSetBorder/UnsetAllBorders.golden diff --git a/table/testdata/TestSetBorder/vertical_borders_only.golden b/table/testdata/TestSetBorder/VerticalBordersOnly.golden similarity index 100% rename from table/testdata/TestSetBorder/vertical_borders_only.golden rename to table/testdata/TestSetBorder/VerticalBordersOnly.golden diff --git a/table/testdata/TestSetStyleFunc/Clear_styles_with_StyleFunc.golden b/table/testdata/TestSetStyleFunc/ClearStylesWithStyleFunc.golden similarity index 100% rename from table/testdata/TestSetStyleFunc/Clear_styles_with_StyleFunc.golden rename to table/testdata/TestSetStyleFunc/ClearStylesWithStyleFunc.golden diff --git a/table/testdata/TestSetStyles/empty_styles.golden b/table/testdata/TestSetStyles/EmptyStyles.golden similarity index 100% rename from table/testdata/TestSetStyles/empty_styles.golden rename to table/testdata/TestSetStyles/EmptyStyles.golden diff --git a/table/testdata/TestSetStyles/new_styles.golden b/table/testdata/TestSetStyles/NewStyles.golden similarity index 100% rename from table/testdata/TestSetStyles/new_styles.golden rename to table/testdata/TestSetStyles/NewStyles.golden diff --git a/table/testdata/TestTableAlignment/No_border.golden b/table/testdata/TestTableAlignment/NoBorder.golden similarity index 100% rename from table/testdata/TestTableAlignment/No_border.golden rename to table/testdata/TestTableAlignment/NoBorder.golden diff --git a/table/testdata/TestTableAlignment/With_border.golden b/table/testdata/TestTableAlignment/WithBorder.golden similarity index 100% rename from table/testdata/TestTableAlignment/With_border.golden rename to table/testdata/TestTableAlignment/WithBorder.golden From 8acc3deec8c54dc12a23a4dcedc8ce27bb7d1f69 Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Wed, 28 May 2025 17:34:21 -0300 Subject: [PATCH 15/25] chore(go.mod): pin lipgloss from branch `v2-table-fixes` --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 0069833cb..15fcbf119 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/atotto/clipboard v0.1.4 github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.1 github.com/charmbracelet/harmonica v0.2.0 - github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1.0.20250516180252-2c4751e06ce4 + github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1.0.20250528174507-b369203354b4 github.com/charmbracelet/x/ansi v0.8.0 github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a github.com/dustin/go-humanize v1.0.1 diff --git a/go.sum b/go.sum index 676e20974..360236db2 100644 --- a/go.sum +++ b/go.sum @@ -10,8 +10,8 @@ github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0= github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= -github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1.0.20250516180252-2c4751e06ce4 h1:7UOIuPdCkW6TEElQT52ACjBs51yJMM6KxvSjhnGVO/M= -github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1.0.20250516180252-2c4751e06ce4/go.mod h1:EJWvaCrhOhNGVZMvcjc0yVryl4qqpMs8tz0r9WyEkdQ= +github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1.0.20250528174507-b369203354b4 h1:9w3krKPOTBKZhQu9iXYkrCmtDM87Nrtf5vJ80Qcn62U= +github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1.0.20250528174507-b369203354b4/go.mod h1:EJWvaCrhOhNGVZMvcjc0yVryl4qqpMs8tz0r9WyEkdQ= github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= From 78e9bc479cd27736c9cdbabd0e7a9bbe68ec88a4 Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Wed, 28 May 2025 17:35:14 -0300 Subject: [PATCH 16/25] test: re-enable tests and record golden files after fixes on lipgloss https://github.com/charmbracelet/lipgloss/pull/526 --- table/table_test.go | 15 ++--------- .../TestModel_View/BorderedCells.golden | 11 ++++++++ .../TestModel_View/BorderedHeaders.golden | 9 +++++++ .../TestModel_View/ExtraPadding.golden | 19 ++++++++++++++ .../ManualHeightLessThanRows.golden | 2 ++ .../ManualWidthGreaterThanColumns.golden | 14 +++++------ .../ManualWidthLessThanColumns.golden | 10 ++++++++ .../RowSeparatorAndNoRightBorder.golden | 25 +++++++------------ .../TestSetBorder/SetAllBorders.golden | 9 +++++++ .../SetRowAndColumnSeparators.golden | 9 +++++++ .../TestSetBorder/UnsetAllBorders.golden | 4 +-- 11 files changed, 89 insertions(+), 38 deletions(-) create mode 100644 table/testdata/TestModel_View/BorderedCells.golden create mode 100644 table/testdata/TestModel_View/BorderedHeaders.golden create mode 100644 table/testdata/TestModel_View/ExtraPadding.golden create mode 100644 table/testdata/TestModel_View/ManualHeightLessThanRows.golden create mode 100644 table/testdata/TestModel_View/ManualWidthLessThanColumns.golden create mode 100644 table/testdata/TestSetBorder/SetAllBorders.golden create mode 100644 table/testdata/TestSetBorder/SetRowAndColumnSeparators.golden diff --git a/table/table_test.go b/table/table_test.go index 2358c693d..fe2cbc79c 100644 --- a/table/table_test.go +++ b/table/table_test.go @@ -249,12 +249,12 @@ func TestSetBorder(t *testing.T) { borders []bool }{ {"UnsetAllBorders", []bool{false}}, - //{"SetAllBorders", []bool{true}}, // FIXME(@andreynering): Fix on Lip Gloss. Unneeded extra row. + {"SetAllBorders", []bool{true}}, {"VerticalBordersOnly", []bool{true, false}}, {"NoTopBorder", []bool{false, true, true}}, {"NoLeftBorder", []bool{true, true, true, false}}, {"RowSeparatorAndNoRightBorder", []bool{true, false, true, true, true}}, - //{"SetRowAndColumnSeparators", []bool{false, false, false, false, true, true}}, // FIXME(@andreynering): Broken style, does this make sense? + {"SetRowAndColumnSeparators", []bool{false, false, false, false, true, true}}, {"InvalidNumberOfArguments", []bool{true, false, false, false, false, true, true}}, } for _, tc := range tests { @@ -673,7 +673,6 @@ func TestModel_View(t *testing.T) { ) }, }, - // FIXME(@andreynering): Fix this scenario in Lip Gloss "ExtraPadding": { modelFunc: func() *Model { s := DefaultStyles() @@ -681,7 +680,6 @@ func TestModel_View(t *testing.T) { s.Cell = lipgloss.NewStyle().Padding(2, 2) return New( - WithHeight(10), WithHeaders("Name", "Country of Origin", "Dunk-able"), WithRows( []string{"Chocolate Digestives", "UK", "Yes"}, @@ -691,7 +689,6 @@ func TestModel_View(t *testing.T) { WithStyles(s), ) }, - skip: true, }, "NoPadding": { modelFunc: func() *Model { @@ -711,7 +708,6 @@ func TestModel_View(t *testing.T) { ) }, }, - // FIXME(@andreynering): Fix this scenario in Lip Gloss "BorderedHeaders": { modelFunc: func() *Model { return New( @@ -726,9 +722,7 @@ func TestModel_View(t *testing.T) { }), ) }, - skip: true, }, - // FIXME(@andreynering): Fix this scenario in Lip Gloss "BorderedCells": { modelFunc: func() *Model { return New( @@ -743,7 +737,6 @@ func TestModel_View(t *testing.T) { }), ) }, - skip: true, }, // FIXME(@andreynering): Fix in Lip Gloss? Potentially add extra empty lines to the bottom of the table. "ManualHeightGreaterThanRows": { @@ -759,7 +752,6 @@ func TestModel_View(t *testing.T) { ) }, }, - // FIXME(@andreynering): Fix this scenario in Lip Gloss. Should truncate table if height is too small. "ManualHeightLessThanRows": { modelFunc: func() *Model { return New( @@ -772,7 +764,6 @@ func TestModel_View(t *testing.T) { ), ) }, - skip: true, }, "ManualWidthGreaterThanColumns": { modelFunc: func() *Model { @@ -787,7 +778,6 @@ func TestModel_View(t *testing.T) { ) }, }, - // FIXME(@andreynering): Fix this scenario in Lip Gloss. "ManualWidthLessThanColumns": { modelFunc: func() *Model { return New( @@ -800,7 +790,6 @@ func TestModel_View(t *testing.T) { ), ) }, - skip: true, }, } diff --git a/table/testdata/TestModel_View/BorderedCells.golden b/table/testdata/TestModel_View/BorderedCells.golden new file mode 100644 index 000000000..237b81911 --- /dev/null +++ b/table/testdata/TestModel_View/BorderedCells.golden @@ -0,0 +1,11 @@ +┌────────────────────────────────────────────────────┐ +│ Name   Country of Origin  Dunk-able │ +├────────────────────────────────────────────────────┤ +│ Chocolate Digestives  UK   Yes  │ +│┌────────────────────┐┌─────────────────┐┌─────────┐│ +││Tim Tams ││Australia ││No ││ +│└────────────────────┘└─────────────────┘└─────────┘│ +│┌────────────────────┐┌─────────────────┐┌─────────┐│ +││Hobnobs ││UK ││Yes ││ +│└────────────────────┘└─────────────────┘└─────────┘│ +└────────────────────────────────────────────────────┘ \ No newline at end of file diff --git a/table/testdata/TestModel_View/BorderedHeaders.golden b/table/testdata/TestModel_View/BorderedHeaders.golden new file mode 100644 index 000000000..56987366a --- /dev/null +++ b/table/testdata/TestModel_View/BorderedHeaders.golden @@ -0,0 +1,9 @@ +┌────────────────────────────────────────────────────┐ +│┌────────────────────┐┌─────────────────┐┌─────────┐│ +││Name ││Country of Origin││Dunk-able││ +│└────────────────────┘└─────────────────┘└─────────┘│ +├────────────────────────────────────────────────────┤ +│ Chocolate Digestives  UK   Yes  │ +│ Tim Tams   Australia   No  │ +│ Hobnobs   UK   Yes  │ +└────────────────────────────────────────────────────┘ \ No newline at end of file diff --git a/table/testdata/TestModel_View/ExtraPadding.golden b/table/testdata/TestModel_View/ExtraPadding.golden new file mode 100644 index 000000000..7aa3c3b46 --- /dev/null +++ b/table/testdata/TestModel_View/ExtraPadding.golden @@ -0,0 +1,19 @@ +┌──────────────────────────────────────────────────────────┐ +│ │ +│ │ +│  Name     Country of Origin    Dunk-able  │ +│ │ +│ │ +├──────────────────────────────────────────────────────────┤ +│ Chocolate Digestives   UK   Yes  │ +│ │ +│ │ +│  Tim Tams     Australia     No   │ +│ │ +│ │ +│ │ +│ │ +│  Hobnobs     UK     Yes   │ +│ │ +│ │ +└──────────────────────────────────────────────────────────┘ \ No newline at end of file diff --git a/table/testdata/TestModel_View/ManualHeightLessThanRows.golden b/table/testdata/TestModel_View/ManualHeightLessThanRows.golden new file mode 100644 index 000000000..8573f95a6 --- /dev/null +++ b/table/testdata/TestModel_View/ManualHeightLessThanRows.golden @@ -0,0 +1,2 @@ +┌────────────────────────────────────────────────────┐ +│ Name   Country of Origin  Dunk-able │ \ No newline at end of file diff --git a/table/testdata/TestModel_View/ManualWidthGreaterThanColumns.golden b/table/testdata/TestModel_View/ManualWidthGreaterThanColumns.golden index fc0f8211c..45d3d9607 100644 --- a/table/testdata/TestModel_View/ManualWidthGreaterThanColumns.golden +++ b/table/testdata/TestModel_View/ManualWidthGreaterThanColumns.golden @@ -1,7 +1,7 @@ -┌─────────────────────────────────────────────────────────────────────────────── -│ Name   Country of Origin   Dunk-able  -├─────────────────────────────────────────────────────────────────────────────── -│ Chocolate Digestives   UK   Yes  -│ Tim Tams   Australia   No  -│ Hobnobs   UK   Yes  -└─────────────────────────────────────────────────────────────────────────────── \ No newline at end of file +┌──────────────────────────────────────────────────────────────────────────────┐ +│ Name   Country of Origin   Dunk-able  │ +├──────────────────────────────────────────────────────────────────────────────┤ +│ Chocolate Digestives   UK   Yes  │ +│ Tim Tams   Australia   No  │ +│ Hobnobs   UK   Yes  │ +└──────────────────────────────────────────────────────────────────────────────┘ \ No newline at end of file diff --git a/table/testdata/TestModel_View/ManualWidthLessThanColumns.golden b/table/testdata/TestModel_View/ManualWidthLessThanColumns.golden new file mode 100644 index 000000000..b54c17dba --- /dev/null +++ b/table/testdata/TestModel_View/ManualWidthLessThanColumns.golden @@ -0,0 +1,10 @@ +┌────────────────────────────┐ +│ Name   Count…  Dunk-a… │ +├────────────────────────────┤ +│ Chocolate  UK   Yes  │ +│ Digestive  │ +│ s  │ +│ Tim Tams   Austra  No  │ +│  lia  │ +│ Hobnobs   UK   Yes  │ +└────────────────────────────┘ \ No newline at end of file diff --git a/table/testdata/TestSetBorder/RowSeparatorAndNoRightBorder.golden b/table/testdata/TestSetBorder/RowSeparatorAndNoRightBorder.golden index 3fcc62996..b05b56a02 100644 --- a/table/testdata/TestSetBorder/RowSeparatorAndNoRightBorder.golden +++ b/table/testdata/TestSetBorder/RowSeparatorAndNoRightBorder.golden @@ -1,16 +1,9 @@ -┌──────────────────────────────────────────── -│ Rank  City   Country   Population  -├──────────────────────────────────────────── -│ 1   Tokyo   Japan   37,274,000  -├────────────────────────────────────────────┤ -│ 2   Delhi   India   32,065,760  -├────────────────────────────────────────────┤ -│ 3   Shanghai   China   28,516,904  -├────────────────────────────────────────────┤ -│ 4   Dhaka   Bangladesh  22,478,116  -├────────────────────────────────────────────┤ -│ 5   São Paulo   Brazil   22,429,800  -├────────────────────────────────────────────┤ -│ …   …   …   …  -├────────────────────────────────────────────┤ -└──────────────────────────────────────────── \ No newline at end of file +┌──────────────────────────────────────────── +│ Rank  City   Country   Population  +├──────────────────────────────────────────── +│ 1   Tokyo   Japan   37,274,000  +├──────────────────────────────────────────── +│ 2   Delhi   India   32,065,760  +├──────────────────────────────────────────── +│ …   …   …   …  +└──────────────────────────────────────────── \ No newline at end of file diff --git a/table/testdata/TestSetBorder/SetAllBorders.golden b/table/testdata/TestSetBorder/SetAllBorders.golden new file mode 100644 index 000000000..93f7321a9 --- /dev/null +++ b/table/testdata/TestSetBorder/SetAllBorders.golden @@ -0,0 +1,9 @@ +┌──────┬──────────────┬────────────┬────────────┐ +│ Rank │ City  │ Country  │ Population │ +├──────┼──────────────┼────────────┼────────────┤ +│ 1  │ Tokyo  │ Japan  │ 37,274,000 │ +├──────┼──────────────┼────────────┼────────────┤ +│ 2  │ Delhi  │ India  │ 32,065,760 │ +├──────┼──────────────┼────────────┼────────────┤ +│ …  │ …  │ …  │ …  │ +└──────┴──────────────┴────────────┴────────────┘ \ No newline at end of file diff --git a/table/testdata/TestSetBorder/SetRowAndColumnSeparators.golden b/table/testdata/TestSetBorder/SetRowAndColumnSeparators.golden new file mode 100644 index 000000000..b52328d51 --- /dev/null +++ b/table/testdata/TestSetBorder/SetRowAndColumnSeparators.golden @@ -0,0 +1,9 @@ + Rank │ City  │ Country  │ Population  +──────┼──────────────┼────────────┼──────────── + 1  │ Tokyo  │ Japan  │ 37,274,000  +──────┼──────────────┼────────────┼──────────── + 2  │ Delhi  │ India  │ 32,065,760  +──────┼──────────────┼────────────┼──────────── + 3  │ Shanghai  │ China  │ 28,516,904  +──────┼──────────────┼────────────┼──────────── + …  │ …  │ …  │ …  \ No newline at end of file diff --git a/table/testdata/TestSetBorder/UnsetAllBorders.golden b/table/testdata/TestSetBorder/UnsetAllBorders.golden index d86fb561a..940f92c38 100644 --- a/table/testdata/TestSetBorder/UnsetAllBorders.golden +++ b/table/testdata/TestSetBorder/UnsetAllBorders.golden @@ -6,5 +6,5 @@  4   Dhaka   Bangladesh  22,478,116   5   São Paulo   Brazil   22,429,800   6   Mexico City   Mexico   22,085,140  - …   …   …   …  - \ No newline at end of file + 7   Cairo   Egypt   21,750,020  + …   …   …   …  \ No newline at end of file From dcb67fb681737e48960d0ad95a72ef6af1c83be1 Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Thu, 29 May 2025 14:04:14 -0300 Subject: [PATCH 17/25] chore: fix linting issues --- table/table.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/table/table.go b/table/table.go index daea993fd..afd7e89f5 100644 --- a/table/table.go +++ b/table/table.go @@ -429,7 +429,7 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { return m, nil } table := m.table.String() - // TODO make this not hard coded? + // XXX: make this not hard coded? height := lipgloss.Height(table) - 6 switch msg := msg.(type) { @@ -473,7 +473,7 @@ func (m Model) View() string { if !m.useStyleFunc { // Update the position-sensitive styles as the cursor position may have // changed in Update. - m.table.StyleFunc(func(row, col int) lipgloss.Style { + m.table.StyleFunc(func(row, _ int) lipgloss.Style { if row == m.cursor { return m.styles.Selected } @@ -505,7 +505,7 @@ func (m Model) Rows() [][]string { return m.rows } -// GetHeaders returns the current headers. +// Headers returns the current headers. func (m Model) Headers() []string { return m.headers } From 82a5f275cfad35b545b4a4cff6fe278c8c8ecf9b Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Thu, 29 May 2025 17:04:57 -0300 Subject: [PATCH 18/25] chore: update lipgloss@v2-table-fixes --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 15fcbf119..5cf884674 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/atotto/clipboard v0.1.4 github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.1 github.com/charmbracelet/harmonica v0.2.0 - github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1.0.20250528174507-b369203354b4 + github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1.0.20250529185650-1c5efe71d7cb github.com/charmbracelet/x/ansi v0.8.0 github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a github.com/dustin/go-humanize v1.0.1 diff --git a/go.sum b/go.sum index 360236db2..1d438270f 100644 --- a/go.sum +++ b/go.sum @@ -10,8 +10,8 @@ github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0= github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= -github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1.0.20250528174507-b369203354b4 h1:9w3krKPOTBKZhQu9iXYkrCmtDM87Nrtf5vJ80Qcn62U= -github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1.0.20250528174507-b369203354b4/go.mod h1:EJWvaCrhOhNGVZMvcjc0yVryl4qqpMs8tz0r9WyEkdQ= +github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1.0.20250529185650-1c5efe71d7cb h1:KwmuaT+M/PFOEgOoYI7x3dOP6gGBXUGrb6Jy+XH/aL8= +github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1.0.20250529185650-1c5efe71d7cb/go.mod h1:EJWvaCrhOhNGVZMvcjc0yVryl4qqpMs8tz0r9WyEkdQ= github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= From 21102238090a930a76cec99383ca10cfcf44bef8 Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Thu, 29 May 2025 17:27:54 -0300 Subject: [PATCH 19/25] refactor(table): get data from lipgloss instead of storing it separately --- table/table.go | 39 ++++++++++++++++++--------------------- table/table_test.go | 40 ++++++++++++++++++++-------------------- 2 files changed, 38 insertions(+), 41 deletions(-) diff --git a/table/table.go b/table/table.go index afd7e89f5..0e80154aa 100644 --- a/table/table.go +++ b/table/table.go @@ -16,8 +16,6 @@ type Model struct { KeyMap KeyMap Help help.Model - headers []string - rows [][]string cursor int focus bool styles Styles @@ -121,19 +119,13 @@ func DefaultStyles() Styles { // NewFromTemplate lets you create a table [Model] from Lip Gloss' // [table.Table]. func NewFromTemplate(t *table.Table, headers []string, rows [][]string) *Model { - m := &Model{ + return &Model{ cursor: 0, KeyMap: DefaultKeyMap(), Help: help.New(), table: t, useStyleFunc: true, } - // We can't get the rows and headers from the table, so the user needs to - // provide them as arguments. - m.rows = rows - m.headers = headers - - return m } // SetBorder is a shorthand function for setting or unsetting borders on a @@ -313,21 +305,19 @@ func WithKeyMap(km KeyMap) Option { // SetHeaders sets the table headers. func (m *Model) SetHeaders(headers ...string) *Model { - m.headers = headers m.table.Headers(headers...) return m } // SetRows sets the table rows. func (m *Model) SetRows(rows ...[]string) *Model { - m.rows = rows m.table.Rows(rows...) return m } // SetCursor sets the cursor position in the table. func (m *Model) SetCursor(n int) *Model { - m.cursor = clamp(n, 0, len(m.rows)-1) + m.cursor = clamp(n, 0, m.RowCount()-1) return m } @@ -347,7 +337,7 @@ func (m *Model) SetWidth(w int) *Model { // SetYOffset sets the YOffset position in the table. func (m *Model) SetYOffset(n int) *Model { - m.yOffset = clamp(n, 0, len(m.rows)-1) + m.yOffset = clamp(n, 0, m.RowCount()-1) m.table.YOffset(m.yOffset) return m } @@ -377,9 +367,11 @@ func (m *Model) OverwriteStyles(s Styles) *Model { // OverwriteStylesFromLipgloss sets the [Model]'s style attributes from an // existing [lipgloss.Table]. func (m *Model) OverwriteStylesFromLipgloss(t *table.Table) { - t.Rows(m.rows...) - t.Headers(m.headers...) - m.table = t + var ( + previousHeaders = m.table.GetHeaders() + previousData = m.table.GetData() + ) + m.table = t.Headers(previousHeaders...).Data(previousData) m.useStyleFunc = true } @@ -502,12 +494,17 @@ func (m Model) Focused() bool { // Rows returns the current rows. func (m Model) Rows() [][]string { - return m.rows + return table.DataToMatrix(m.table.GetData()) +} + +// RowCount returns the number of rows in the table. +func (m Model) RowCount() int { + return m.table.GetData().Rows() } // Headers returns the current headers. func (m Model) Headers() []string { - return m.headers + return m.table.GetHeaders() } // Cursor returns the index of the selected row. @@ -518,11 +515,11 @@ func (m Model) Cursor() int { // 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) { + if m.cursor < 0 || m.cursor >= m.RowCount() { return nil } - return m.rows[m.cursor] + return table.DataToMatrix(m.table.GetData())[m.cursor] } // Movement @@ -550,7 +547,7 @@ func (m *Model) GotoTop() { // GotoBottom moves the selection to the last row. func (m *Model) GotoBottom() { - m.MoveDown(len(m.rows)) + m.MoveDown(m.RowCount()) } // Helpers diff --git a/table/table_test.go b/table/table_test.go index fe2cbc79c..606ff9aa5 100644 --- a/table/table_test.go +++ b/table/table_test.go @@ -74,8 +74,8 @@ func TestModel_FromValues(t *testing.T) { []string{"foo3", "bar3"}, )) - if len(table.rows) != 3 { - t.Fatalf("expect table to have 3 rows but it has %d", len(table.rows)) + if table.RowCount() != 3 { + t.Fatalf("expect table to have 3 rows but it has %d", table.RowCount()) } expect := [][]string{ @@ -83,8 +83,8 @@ func TestModel_FromValues(t *testing.T) { {"foo2", "bar2"}, {"foo3", "bar3"}, } - if !reflect.DeepEqual(table.rows, expect) { - t.Fatalf("\n\nwant %v\n\ngot %v", expect, table.rows) + if !reflect.DeepEqual(table.Rows(), expect) { + t.Fatalf("\n\nwant %v\n\ngot %v", expect, table.Rows()) } } @@ -97,16 +97,16 @@ func TestModel_FromValues_WithTabSeparator(t *testing.T) { ), ) - if len(table.rows) != 2 { - t.Fatalf("expect table to have 2 rows but it has %d", len(table.rows)) + if table.RowCount() != 2 { + t.Fatalf("expect table to have 2 rows but it has %d", table.RowCount()) } expect := [][]string{ {"foo1.", "bar1"}, {"foo,bar,baz", "bar,2"}, } - if !reflect.DeepEqual(table.rows, expect) { - t.Fatalf("\n\nwant %v\n\ngot %v", expect, table.rows) + if !reflect.DeepEqual(table.Rows(), expect) { + t.Fatalf("\n\nwant %v\n\ngot %v", expect, table.Rows()) } t.Run("new with options", func(t *testing.T) { tb := New( @@ -606,38 +606,38 @@ func TestCursorNavigation(t *testing.T) { func TestModel_SetRows(t *testing.T) { table := New(WithHeaders("col1", "col2", "col3")) - if len(table.rows) != 0 { - t.Fatalf("want 0, got %d", len(table.rows)) + if table.RowCount() != 0 { + t.Fatalf("want 0, got %d", table.RowCount()) } table.SetRows([]string{"r1"}, []string{"r2"}) - if len(table.rows) != 2 { - t.Fatalf("want 2, got %d", len(table.rows)) + if table.RowCount() != 2 { + t.Fatalf("want 2, got %d", table.RowCount()) } want := [][]string{{"r1"}, {"r2"}} - if !reflect.DeepEqual(table.rows, want) { - t.Fatalf("\n\nwant %v\n\ngot %v", want, table.rows) + if !reflect.DeepEqual(table.Rows(), want) { + t.Fatalf("\n\nwant %v\n\ngot %v", want, table.Rows()) } } func TestModel_SetHeaders(t *testing.T) { table := New() - if len(table.headers) != 0 { - t.Fatalf("want 0, got %d", len(table.headers)) + if len(table.Headers()) != 0 { + t.Fatalf("want 0, got %d", len(table.Headers())) } table.SetHeaders("Foo", "Bar") - if len(table.headers) != 2 { - t.Fatalf("want 2, got %d", len(table.headers)) + if len(table.Headers()) != 2 { + t.Fatalf("want 2, got %d", len(table.Headers())) } want := []string{"Foo", "Bar"} - if !reflect.DeepEqual(table.headers, want) { - t.Fatalf("\n\nwant %v\n\ngot %v", want, table.headers) + if !reflect.DeepEqual(table.Headers(), want) { + t.Fatalf("\n\nwant %v\n\ngot %v", want, table.Headers()) } } From fe39af2a4de5342f1e4cb3285e755e5fe0c5b60d Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Thu, 29 May 2025 17:41:43 -0300 Subject: [PATCH 20/25] refactor(table): rename `OverwriteStylesFromLipgloss` to `LipglossTable` Also, keeps any previously set data on the Lip Gloss table, if set. --- table/table.go | 13 +++-- table/table_test.go | 49 +++++++++++++------ .../WithPreviousData.golden} | 0 .../WithoutPreviousData.golden | 20 ++++++++ 4 files changed, 62 insertions(+), 20 deletions(-) rename table/testdata/{TestOverwriteStylesFromLipgloss.golden => TestLipglossTable/WithPreviousData.golden} (100%) create mode 100644 table/testdata/TestLipglossTable/WithoutPreviousData.golden diff --git a/table/table.go b/table/table.go index 0e80154aa..69f39c88f 100644 --- a/table/table.go +++ b/table/table.go @@ -364,14 +364,19 @@ func (m *Model) OverwriteStyles(s Styles) *Model { return m } -// OverwriteStylesFromLipgloss sets the [Model]'s style attributes from an -// existing [lipgloss.Table]. -func (m *Model) OverwriteStylesFromLipgloss(t *table.Table) { +// LipglossTable sets the inner [lipgloss.Table]. +func (m *Model) LipglossTable(t *table.Table) { var ( previousHeaders = m.table.GetHeaders() previousData = m.table.GetData() ) - m.table = t.Headers(previousHeaders...).Data(previousData) + if len(t.GetHeaders()) == 0 { + t = t.Headers(previousHeaders...) + } + if t.GetData() == nil || t.GetData().Rows() == 0 { + t = t.Rows(table.DataToMatrix(previousData)...) + } + m.table = t m.useStyleFunc = true } diff --git a/table/table_test.go b/table/table_test.go index 606ff9aa5..10081705e 100644 --- a/table/table_test.go +++ b/table/table_test.go @@ -368,28 +368,45 @@ func TestNewFromTemplate(t *testing.T) { golden.RequireEqual(t, []byte(bubblesTable.View())) } -func TestOverwriteStylesFromLipgloss(t *testing.T) { - baseStyle := lipgloss.NewStyle().Padding(0, 1) - headerStyle := baseStyle.Foreground(lipgloss.Color("252")).Bold(true) - lipglossTable := table.New(). - Border(lipgloss.NormalBorder()). - BorderStyle(lipgloss.NewStyle().Foreground(lipgloss.Color("238"))). - Width(80). - StyleFunc(func(row, col int) lipgloss.Style { +func TestLipglossTable(t *testing.T) { + var ( + baseStyle = lipgloss.NewStyle().Padding(0, 1) + headerStyle = baseStyle.Foreground(lipgloss.Color("252")).Bold(true) + borderStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("238")) + styleFunc = func(row, col int) lipgloss.Style { if row == table.HeaderRow { return headerStyle } - - even := row%2 == 0 - - if even { + if row%2 == 0 { return baseStyle.Foreground(lipgloss.Color("245")) } return baseStyle.Foreground(lipgloss.Color("252")) - }) - bubblesTable := New().SetHeaders(headers...).SetRows(rows...) - bubblesTable.OverwriteStylesFromLipgloss(lipglossTable) - golden.RequireEqual(t, []byte(bubblesTable.View())) + } + ) + + t.Run("WithoutPreviousData", func(t *testing.T) { + lipglossTable := table.New(). + Border(lipgloss.NormalBorder()). + BorderStyle(borderStyle). + Width(80). + StyleFunc(styleFunc) + bubblesTable := New().SetHeaders(headers...).SetRows(rows...) + bubblesTable.LipglossTable(lipglossTable) + golden.RequireEqual(t, []byte(bubblesTable.View())) + }) + + t.Run("WithPreviousData", func(t *testing.T) { + lipglossTable := table.New(). + Border(lipgloss.NormalBorder()). + BorderStyle(borderStyle). + Width(80). + StyleFunc(styleFunc). + Headers(headers...). + Rows(rows...) + bubblesTable := New() + bubblesTable.LipglossTable(lipglossTable) + golden.RequireEqual(t, []byte(bubblesTable.View())) + }) } // Examples diff --git a/table/testdata/TestOverwriteStylesFromLipgloss.golden b/table/testdata/TestLipglossTable/WithPreviousData.golden similarity index 100% rename from table/testdata/TestOverwriteStylesFromLipgloss.golden rename to table/testdata/TestLipglossTable/WithPreviousData.golden diff --git a/table/testdata/TestLipglossTable/WithoutPreviousData.golden b/table/testdata/TestLipglossTable/WithoutPreviousData.golden new file mode 100644 index 000000000..3cb3da3a3 --- /dev/null +++ b/table/testdata/TestLipglossTable/WithoutPreviousData.golden @@ -0,0 +1,20 @@ +┌───────────────────┬───────────────────┬───────────────────┬──────────────────┐ +│ Rank  │ City  │ Country  │ Population  │ +├───────────────────┼───────────────────┼───────────────────┼──────────────────┤ +│ 1  │ Tokyo  │ Japan  │ 37,274,000  │ +│ 2  │ Delhi  │ India  │ 32,065,760  │ +│ 3  │ Shanghai  │ China  │ 28,516,904  │ +│ 4  │ Dhaka  │ Bangladesh  │ 22,478,116  │ +│ 5  │ São Paulo  │ Brazil  │ 22,429,800  │ +│ 6  │ Mexico City  │ Mexico  │ 22,085,140  │ +│ 7  │ Cairo  │ Egypt  │ 21,750,020  │ +│ 8  │ Beijing  │ China  │ 21,333,332  │ +│ 9  │ Mumbai  │ India  │ 20,961,472  │ +│ 10  │ Osaka  │ Japan  │ 19,059,856  │ +│ 11  │ Chongqing  │ China  │ 16,874,740  │ +│ 12  │ Karachi  │ Pakistan  │ 16,839,950  │ +│ 13  │ Istanbul  │ Turkey  │ 15,636,243  │ +│ 14  │ Kinshasa  │ DR Congo  │ 15,628,085  │ +│ 15  │ Lagos  │ Nigeria  │ 15,387,639  │ +│ 16  │ Buenos Aires  │ Argentina  │ 15,369,919  │ +└───────────────────┴───────────────────┴───────────────────┴──────────────────┘ \ No newline at end of file From aa84c7f57bba9282ddfd6134ae4f5e3c92a341d0 Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Thu, 29 May 2025 18:00:57 -0300 Subject: [PATCH 21/25] refactor(table): remove uneeded extra args from `NewFromTemplate` --- table/table.go | 2 +- table/table_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/table/table.go b/table/table.go index 69f39c88f..1e9ddbcff 100644 --- a/table/table.go +++ b/table/table.go @@ -118,7 +118,7 @@ func DefaultStyles() Styles { // NewFromTemplate lets you create a table [Model] from Lip Gloss' // [table.Table]. -func NewFromTemplate(t *table.Table, headers []string, rows [][]string) *Model { +func NewFromTemplate(t *table.Table) *Model { return &Model{ cursor: 0, KeyMap: DefaultKeyMap(), diff --git a/table/table_test.go b/table/table_test.go index 10081705e..aa37746c1 100644 --- a/table/table_test.go +++ b/table/table_test.go @@ -364,7 +364,7 @@ func TestNewFromTemplate(t *testing.T) { return baseStyle.Foreground(lipgloss.Color("252")) }) - bubblesTable := NewFromTemplate(lipglossTable, headers, pokemonData) + bubblesTable := NewFromTemplate(lipglossTable) golden.RequireEqual(t, []byte(bubblesTable.View())) } From f0f84c81f489d3e5bc75ea83b29f5aa5b89e2648 Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Fri, 30 May 2025 14:27:11 -0300 Subject: [PATCH 22/25] refactor(table): remove inner border attrs, get directly from lipgloss --- table/table.go | 32 ++++++-------------------------- 1 file changed, 6 insertions(+), 26 deletions(-) diff --git a/table/table.go b/table/table.go index 1e9ddbcff..7f1e7783a 100644 --- a/table/table.go +++ b/table/table.go @@ -92,16 +92,6 @@ func DefaultKeyMap() KeyMap { // Styles contains style definitions for this table component. Load default // styles to your table with [DefaultStyles]. type Styles struct { - 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 @@ -152,7 +142,6 @@ func NewFromTemplate(t *table.Table) *Model { // // With more than six arguments nothing will be set. func (m *Model) SetBorder(s ...bool) *Model { - m.table.Border(m.styles.border) top, right, bottom, left, rowSeparator, columnSeparator := m.whichSides(s...) m.table. BorderTop(top). @@ -166,63 +155,54 @@ func (m *Model) SetBorder(s ...bool) *Model { // Border sets the kind of border to use for the table. See [lipgloss.Border]. func (m *Model) Border(border lipgloss.Border) *Model { - m.styles.border = border m.table.Border(border) 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 } // 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 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 } @@ -572,8 +552,8 @@ func clamp(v, low, high int) int { // 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.table.GetBorderRow() + columnSeparator = m.table.GetBorderColumn() switch len(s) { case 1: @@ -612,10 +592,10 @@ 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.table.GetBorderTop() + right = m.table.GetBorderRight() + bottom = m.table.GetBorderBottom() + left = m.table.GetBorderLeft() } return top, right, bottom, left, rowSeparator, columnSeparator } From bf386dbad5c8e7a46e47016ffb134d2848fe7dca Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Fri, 30 May 2025 16:34:35 -0300 Subject: [PATCH 23/25] fix(table): fix cursor and offset handling --- go.mod | 2 +- go.sum | 4 ++-- table/table.go | 21 ++++++++++++++------- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/go.mod b/go.mod index 5cf884674..a610ded48 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/atotto/clipboard v0.1.4 github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.1 github.com/charmbracelet/harmonica v0.2.0 - github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1.0.20250529185650-1c5efe71d7cb + github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1.0.20250530192739-0ec908265ee6 github.com/charmbracelet/x/ansi v0.8.0 github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a github.com/dustin/go-humanize v1.0.1 diff --git a/go.sum b/go.sum index 1d438270f..cb221ea11 100644 --- a/go.sum +++ b/go.sum @@ -10,8 +10,8 @@ github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0= github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= -github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1.0.20250529185650-1c5efe71d7cb h1:KwmuaT+M/PFOEgOoYI7x3dOP6gGBXUGrb6Jy+XH/aL8= -github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1.0.20250529185650-1c5efe71d7cb/go.mod h1:EJWvaCrhOhNGVZMvcjc0yVryl4qqpMs8tz0r9WyEkdQ= +github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1.0.20250530192739-0ec908265ee6 h1:xvkno6nNCinMEhpHwnOTvasaFSeLVMw5DDaJZaDm4iM= +github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1.0.20250530192739-0ec908265ee6/go.mod h1:EJWvaCrhOhNGVZMvcjc0yVryl4qqpMs8tz0r9WyEkdQ= github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= diff --git a/table/table.go b/table/table.go index 7f1e7783a..0f1cee5a3 100644 --- a/table/table.go +++ b/table/table.go @@ -19,7 +19,6 @@ type Model struct { cursor int focus bool styles Styles - yOffset int useStyleFunc bool table *table.Table @@ -317,8 +316,12 @@ func (m *Model) SetWidth(w int) *Model { // SetYOffset sets the YOffset position in the table. func (m *Model) SetYOffset(n int) *Model { - m.yOffset = clamp(n, 0, m.RowCount()-1) - m.table.YOffset(m.yOffset) + var ( + minimum = 0 + maximum = m.RowCount() - m.table.VisibleRows() + yOffset = clamp(n, minimum, maximum) + ) + m.table.YOffset(yOffset) return m } @@ -513,16 +516,20 @@ func (m Model) SelectedRow() []string { // It can not go above the first row. func (m *Model) MoveUp(n int) { m.SetCursor(m.cursor - n) - m.SetYOffset(m.yOffset - n) - m.table.YOffset(m.yOffset) + + if m.cursor < m.table.FirstVisibleRowIndex() { + m.SetYOffset(m.table.GetYOffset() - n) + } } // MoveDown moves the selection down by any number of rows. // It can not go below the last row. func (m *Model) MoveDown(n int) { m.SetCursor(m.cursor + n) - m.SetYOffset(m.yOffset + n) - m.table.YOffset(m.yOffset) + + if m.cursor > m.table.LastVisibleRowIndex() { + m.SetYOffset(m.table.GetYOffset() + n) + } } // GotoTop moves the selection to the first row. From e7748f085640b94ce6dcdc7ed599ca8169cddf45 Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Fri, 30 May 2025 16:55:34 -0300 Subject: [PATCH 24/25] fix(table): add fixes to `pageup`/`pagedown` --- table/table.go | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/table/table.go b/table/table.go index 0f1cee5a3..75ed35bb9 100644 --- a/table/table.go +++ b/table/table.go @@ -408,9 +408,7 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { if !m.focus { return m, nil } - table := m.table.String() - // XXX: make this not hard coded? - height := lipgloss.Height(table) - 6 + height := m.table.VisibleRows() - 1 switch msg := msg.(type) { case tea.KeyPressMsg: @@ -420,9 +418,17 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { case key.Matches(msg, m.KeyMap.LineDown): m.MoveDown(1) case key.Matches(msg, m.KeyMap.PageUp): - m.MoveUp(height) + if m.cursor > m.table.FirstVisibleRowIndex() { + m.SetCursor(m.table.FirstVisibleRowIndex()) + } else { + m.MoveUp(height) + } case key.Matches(msg, m.KeyMap.PageDown): - m.MoveDown(height) + if m.cursor < m.table.LastVisibleRowIndex() { + m.SetCursor(m.table.LastVisibleRowIndex()) + } else { + m.MoveDown(height) + } case key.Matches(msg, m.KeyMap.HalfPageUp): m.MoveUp(height / 2) //nolint:mnd case key.Matches(msg, m.KeyMap.HalfPageDown): From 3e517500ddad2eb5db3958316f7de77423856f6b Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Tue, 3 Jun 2025 11:10:05 -0300 Subject: [PATCH 25/25] chore(go.mod): update lipgloss from `v2-exp` --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index a610ded48..f78ba7a45 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/atotto/clipboard v0.1.4 github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.1 github.com/charmbracelet/harmonica v0.2.0 - github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1.0.20250530192739-0ec908265ee6 + github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1.0.20250603140710-946081c6f1a5 github.com/charmbracelet/x/ansi v0.8.0 github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a github.com/dustin/go-humanize v1.0.1 diff --git a/go.sum b/go.sum index cb221ea11..118c8a96b 100644 --- a/go.sum +++ b/go.sum @@ -10,8 +10,8 @@ github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0= github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= -github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1.0.20250530192739-0ec908265ee6 h1:xvkno6nNCinMEhpHwnOTvasaFSeLVMw5DDaJZaDm4iM= -github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1.0.20250530192739-0ec908265ee6/go.mod h1:EJWvaCrhOhNGVZMvcjc0yVryl4qqpMs8tz0r9WyEkdQ= +github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1.0.20250603140710-946081c6f1a5 h1:vq3WdrGHQ6T0ZBYdRhn2toyQXDD+uneu3IdcMni7J5g= +github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1.0.20250603140710-946081c6f1a5/go.mod h1:EJWvaCrhOhNGVZMvcjc0yVryl4qqpMs8tz0r9WyEkdQ= github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=