diff --git a/table/table.go b/table/table.go index 1508954..ea9fd1e 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) }