// Package table provides a simple table component for Bubble Tea applications. package table import ( "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" ) // Model defines a state for the table widget. type Model struct { KeyMap KeyMap Help help.Model headers []string rows [][]string cursor int focus bool styles Styles yOffset int table *table.Table } // KeyMap defines keybindings. It satisfies to the help.KeyMap interface, which // is used to render the help menu. type KeyMap struct { LineUp key.Binding LineDown key.Binding PageUp key.Binding PageDown key.Binding HalfPageUp key.Binding HalfPageDown key.Binding GotoTop key.Binding GotoBottom key.Binding } // ShortHelp implements the KeyMap interface. func (km KeyMap) ShortHelp() []key.Binding { return []key.Binding{km.LineUp, km.LineDown} } // FullHelp implements the KeyMap interface. func (km KeyMap) FullHelp() [][]key.Binding { return [][]key.Binding{ {km.LineUp, km.LineDown, km.GotoTop, km.GotoBottom}, {km.PageUp, km.PageDown, km.HalfPageUp, km.HalfPageDown}, } } // DefaultKeyMap returns a default set of keybindings. func DefaultKeyMap() KeyMap { return KeyMap{ LineUp: key.NewBinding( key.WithKeys("up", "k"), key.WithHelp("↑/k", "up"), ), LineDown: key.NewBinding( key.WithKeys("down", "j"), key.WithHelp("↓/j", "down"), ), PageUp: key.NewBinding( key.WithKeys("b", "pgup"), key.WithHelp("b/pgup", "page up"), ), PageDown: key.NewBinding( key.WithKeys("f", "pgdown", "space"), key.WithHelp("f/pgdn", "page down"), ), HalfPageUp: key.NewBinding( key.WithKeys("u", "ctrl+u"), key.WithHelp("u", "½ page up"), ), HalfPageDown: key.NewBinding( key.WithKeys("d", "ctrl+d"), key.WithHelp("d", "½ page down"), ), GotoTop: key.NewBinding( key.WithKeys("home", "g"), key.WithHelp("g/home", "go to start"), ), GotoBottom: key.NewBinding( key.WithKeys("end", "G"), key.WithHelp("G/end", "go to end"), ), } } // 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 } 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), } } 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 } // 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}})) type Option func(*Model) // New creates a new model for the table widget. func New(opts ...Option) Model { m := Model{ cursor: 0, KeyMap: DefaultKeyMap(), Help: help.New(), styles: DefaultStyles(), } for _, opt := range opts { opt(&m) } 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 WithHeaders(headers ...string) Option { return func(m *Model) { 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 ...[]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.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.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) { m.focus = f } } // 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(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.table.StyleFunc(s) } } // WithKeyMap sets the key map. func WithKeyMap(km KeyMap) Option { return func(m *Model) { m.KeyMap = km } } // Update is the Bubble Tea update loop. 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: switch { case key.Matches(msg, m.KeyMap.LineUp): m.MoveUp(1) case key.Matches(msg, m.KeyMap.LineDown): m.MoveDown(1) case key.Matches(msg, m.KeyMap.PageUp): m.MoveUp(height) case key.Matches(msg, m.KeyMap.PageDown): m.MoveDown(height) case key.Matches(msg, m.KeyMap.HalfPageUp): m.MoveUp(height / 2) //nolint:mnd case key.Matches(msg, m.KeyMap.HalfPageDown): m.MoveDown(height / 2) //nolint:mnd case key.Matches(msg, m.KeyMap.GotoTop): m.GotoTop() case key.Matches(msg, m.KeyMap.GotoBottom): m.GotoBottom() } } 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() { m.focus = true } // Blur blurs the table, preventing selection or movement. func (m *Model) Blur() { m.focus = false } // View renders the component. func (m Model) View() string { return m.table.String() } // HelpView is a helper method for rendering the help menu from the keymap. // Note that this view is not rendered by default and you must call it // manually in your application, where applicable. func (m Model) HelpView() string { return m.Help.View(m.KeyMap) } // Rows returns the current rows. func (m Model) Rows() [][]string { return m.rows } // GetHeaders returns the current headers. func (m Model) Headers() []string { return m.headers } // WithWidth sets the width of the viewport of the table. func (m *Model) WithWidth(w int) { m.table.Width(w) } // WithHeight sets the height of the viewport 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) { m.cursor = clamp(n, 0, len(m.rows)-1) } // 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.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) { // 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) } // GotoBottom moves the selection to the last row. 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...) } // 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) }