diff --git a/table/table.go b/table/table.go index 6103c83..9c13d98 100644 --- a/table/table.go +++ b/table/table.go @@ -8,29 +8,38 @@ import ( "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "github.com/mattn/go-runewidth" + "github.com/charmbracelet/lipgloss/table" ) +const HEADER int = 0 + // Model defines a state for the table widget. type Model struct { KeyMap KeyMap Help help.Model - cols []Column - rows []Row - cursor int - focus bool - styles Styles + YOffset int + Height int + headers []string + rows [][]string + cursor int + focus bool + styles Styles + table *table.Table + start int + end int + + // deprecated: don't use viewport, use table instead. viewport viewport.Model - start int - end int } // Row represents one line in the table. +// Deprecated: use []string. type Row []string // Column defines the table structure. +// Deprecated: use []string. type Column struct { Title string Width int @@ -104,24 +113,38 @@ func DefaultKeyMap() KeyMap { // Styles contains style definitions for this list component. By default, these // values are generated by DefaultStyles. type Styles struct { - Header lipgloss.Style - Cell lipgloss.Style - Selected lipgloss.Style + // TODO is there a way to extract the border from the style? + Border lipgloss.Border + // Why doesn't setting the BorderStyle overwrite the default lip gloss table border? + BorderStyle lipgloss.Style + Header lipgloss.Style + Cell lipgloss.Style + Selected lipgloss.Style } // DefaultStyles returns a set of default style definitions for this table. 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), + BorderStyle: lipgloss.NewStyle().BorderStyle(lipgloss.HiddenBorder()), + Selected: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("212")), + Header: lipgloss.NewStyle().Bold(true).Padding(0, 1), + Cell: lipgloss.NewStyle().Padding(0, 1), } } // SetStyles sets the table styles. func (m *Model) SetStyles(s Styles) { m.styles = s - m.UpdateViewport() + m.table.BorderStyle(s.BorderStyle) + m.table.StyleFunc(func(row, col int) lipgloss.Style { + if row == HEADER { + return s.Header + } + if row == m.cursor { + return s.Selected + } + return s.Cell + }) } // Option is used to set options in New. For example: @@ -130,43 +153,82 @@ func (m *Model) SetStyles(s Styles) { type Option func(*Model) // New creates a new model for the table widget. -func New(opts ...Option) Model { - m := Model{ - cursor: 0, - viewport: viewport.New(0, 20), - +func New(opts ...Option) *Model { + m := &Model{ + cursor: 0, + table: table.New(), KeyMap: DefaultKeyMap(), Help: help.New(), styles: DefaultStyles(), } for _, opt := range opts { - opt(&m) + opt(m) } - m.UpdateViewport() + return m +} +// Headers sets the table headers. +func (m *Model) Headers(headers ...string) *Model { + m.headers = headers + m.table.Headers(headers...) return m } // WithColumns sets the table columns (headers). +// Deprecated: use Headers instead. func WithColumns(cols []Column) Option { return func(m *Model) { - m.cols = cols + m.Headers(colToString(cols)...) } } +// Rows appends rows to the table +func (m *Model) Rows(rows ...[]string) *Model { + m.rows = append(m.rows, rows...) + m.table.Rows(rows...) + return m +} + +// rowToString helper to unwrap the Row type. +func rowToString(rows []Row) [][]string { + var out [][]string + for _, row := range rows { + var newRow []string + for _, val := range row { + newRow = append(newRow, val) + } + out = append(out, newRow) + } + return out +} + +// rowToString helper to unwrap the Row type. +func colToString(cols []Column) []string { + var out []string + for _, col := range cols { + out = append(out, col.Title) + } + return out +} + // WithRows sets the table rows (data). +// Deprecated: use Rows instead. func WithRows(rows []Row) Option { return func(m *Model) { + rows := rowToString(rows) m.rows = rows + m.table.Rows(rows...) } } +/* options */ + // WithHeight sets the height of the table. func WithHeight(h int) Option { return func(m *Model) { - m.viewport.Height = h - lipgloss.Height(m.headersView()) + m.table.Height(h) } } @@ -187,7 +249,7 @@ func WithFocused(f bool) Option { // WithStyles sets the table styles. func WithStyles(s Styles) Option { return func(m *Model) { - m.styles = s + m.SetStyles(s) } } @@ -198,7 +260,7 @@ func WithKeyMap(km KeyMap) Option { } } -// Update is the Bubble Tea update loop. +// Update for the Bubble Tea update loop. func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { if !m.focus { return m, nil @@ -240,18 +302,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. @@ -261,29 +321,6 @@ 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 { @@ -294,48 +331,22 @@ func (m Model) SelectedRow() Row { return m.rows[m.cursor] } -// Rows returns the current rows. -func (m Model) Rows() []Row { - return m.rows -} - -// Columns returns the current columns. -func (m Model) Columns() []Column { - return m.cols -} - -// SetRows sets a new rows state. +// SetRows sets the table content to the new value, overwriting previous values. 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() + rows := rowToString(r) + m.rows = rows + m.table.ClearRows().Rows(rows...) } // SetWidth sets the width of the viewport of the table. func (m *Model) SetWidth(w int) { - m.viewport.Width = w - m.UpdateViewport() + m.table.Width(w) } // SetHeight sets the height of the viewport of the table. func (m *Model) SetHeight(h int) { - m.viewport.Height = h - lipgloss.Height(m.headersView()) - m.UpdateViewport() -} - -// Height returns the viewport height of the table. -func (m Model) Height() int { - return m.viewport.Height -} - -// Width returns the viewport width of the table. -func (m Model) Width() int { - return m.viewport.Width + m.Height = h + m.table.Height(h) } // Cursor returns the index of the selected row. @@ -346,49 +357,53 @@ func (m Model) Cursor() int { // SetCursor sets the cursor position in the table. func (m *Model) SetCursor(n int) { m.cursor = clamp(n, 0, len(m.rows)-1) - m.UpdateViewport() } // 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.SetCursor(m.cursor-n) m.cursor = clamp(m.cursor-n, 0, len(m.rows)-1) switch { case m.start == 0: - m.viewport.SetYOffset(clamp(m.viewport.YOffset, 0, m.cursor)) + m.YOffset = clamp(m.viewport.YOffset, 0, m.cursor) case m.start < m.viewport.Height: - m.viewport.YOffset = (clamp(clamp(m.viewport.YOffset+n, 0, m.cursor), 0, m.viewport.Height)) - case m.viewport.YOffset >= 1: - m.viewport.YOffset = clamp(m.viewport.YOffset+n, 1, m.viewport.Height) + m.viewport.YOffset = (clamp(clamp(m.YOffset+n, 0, m.cursor), 0, m.Height)) + case m.YOffset >= 1: + m.YOffset = clamp(m.YOffset+n, 1, m.Height) } - m.UpdateViewport() + m.table.Offset(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() + // TODO make this call SetCursor instead? + m.SetCursor(m.cursor + n) switch { - case m.end == len(m.rows) && m.viewport.YOffset > 0: - m.viewport.SetYOffset(clamp(m.viewport.YOffset-n, 1, m.viewport.Height)) - case m.cursor > (m.end-m.start)/2 && m.viewport.YOffset > 0: - m.viewport.SetYOffset(clamp(m.viewport.YOffset-n, 1, m.cursor)) - case m.viewport.YOffset > 1: - case m.cursor > m.viewport.YOffset+m.viewport.Height-1: - m.viewport.SetYOffset(clamp(m.viewport.YOffset+1, 0, 1)) + case m.end == len(m.rows) && m.YOffset > 0: + m.YOffset = clamp(m.YOffset-n, 1, m.Height) + case m.cursor > (m.end-m.start)/2 && m.YOffset > 0: + m.YOffset = clamp(m.YOffset-n, 1, m.cursor) + case m.YOffset > 1: + case m.cursor > m.YOffset+m.Height-1: + m.YOffset = clamp(m.YOffset+1, 0, 1) } + // update table offset + m.table.Offset(m.YOffset) } // GotoTop moves the selection to the first row. func (m *Model) GotoTop() { - m.MoveUp(m.cursor) + // m.MoveUp(m.cursor) + m.cursor = HEADER + 1 } // GotoBottom moves the selection to the last row. func (m *Model) GotoBottom() { - m.MoveDown(len(m.rows)) + // m.MoveDown(len(m.rows)) + m.cursor = len(m.rows) } // FromValues create the table rows from a simple string. It uses `\n` by @@ -407,38 +422,38 @@ func (m *Model) FromValues(value, separator string) { 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) 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 -} +// 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 max(a, b int) int { if a > b {