511 lines
13 KiB
Go
511 lines
13 KiB
Go
package table
|
|
|
|
import (
|
|
"github.com/charmbracelet/bubbles/key"
|
|
"github.com/charmbracelet/bubbles/textinput"
|
|
"github.com/charmbracelet/lipgloss"
|
|
)
|
|
|
|
// RowStyleFuncInput is the input to the style function that can
|
|
// be applied to each row. This is useful for things like zebra
|
|
// striping or other data-based styles.
|
|
//
|
|
// Note that we use a struct here to allow for future expansion
|
|
// while keeping backwards compatibility.
|
|
type RowStyleFuncInput struct {
|
|
// Index is the index of the row, starting at 0.
|
|
Index int
|
|
|
|
// Row is the full row data.
|
|
Row Row
|
|
|
|
// IsHighlighted is true if the row is currently highlighted.
|
|
IsHighlighted bool
|
|
}
|
|
|
|
// WithRowStyleFunc sets a function that can be used to apply a style to each row
|
|
// based on the row data. This is useful for things like zebra striping or other
|
|
// data-based styles. It can be safely set to nil to remove it later.
|
|
// This style is applied after the base style and before individual row styles.
|
|
// This will override any HighlightStyle settings.
|
|
func (m Model) WithRowStyleFunc(f func(RowStyleFuncInput) lipgloss.Style) Model {
|
|
m.rowStyleFunc = f
|
|
|
|
return m
|
|
}
|
|
|
|
// WithHighlightedRow sets the highlighted row to the given index.
|
|
func (m Model) WithHighlightedRow(index int) Model {
|
|
m.rowCursorIndex = index
|
|
|
|
if m.rowCursorIndex >= len(m.GetVisibleRows()) {
|
|
m.rowCursorIndex = len(m.GetVisibleRows()) - 1
|
|
}
|
|
|
|
if m.rowCursorIndex < 0 {
|
|
m.rowCursorIndex = 0
|
|
}
|
|
|
|
m.currentPage = m.expectedPageForRowIndex(m.rowCursorIndex)
|
|
|
|
return m
|
|
}
|
|
|
|
// HeaderStyle sets the style to apply to the header text, such as color or bold.
|
|
func (m Model) HeaderStyle(style lipgloss.Style) Model {
|
|
m.headerStyle = style.Copy()
|
|
|
|
return m
|
|
}
|
|
|
|
// WithRows sets the rows to show as data in the table.
|
|
func (m Model) WithRows(rows []Row) Model {
|
|
m.rows = rows
|
|
m.visibleRowCacheUpdated = false
|
|
|
|
if m.rowCursorIndex >= len(m.rows) {
|
|
m.rowCursorIndex = len(m.rows) - 1
|
|
}
|
|
|
|
if m.rowCursorIndex < 0 {
|
|
m.rowCursorIndex = 0
|
|
}
|
|
|
|
if m.pageSize != 0 {
|
|
maxPage := m.MaxPages()
|
|
|
|
// MaxPages is 1-index, currentPage is 0 index
|
|
if maxPage <= m.currentPage {
|
|
m.pageLast()
|
|
}
|
|
}
|
|
|
|
return m
|
|
}
|
|
|
|
// WithKeyMap sets the key map to use for controls when focused.
|
|
func (m Model) WithKeyMap(keyMap KeyMap) Model {
|
|
m.keyMap = keyMap
|
|
|
|
return m
|
|
}
|
|
|
|
// KeyMap returns a copy of the current key map in use.
|
|
func (m Model) KeyMap() KeyMap {
|
|
return m.keyMap
|
|
}
|
|
|
|
// SelectableRows sets whether or not rows are selectable. If set, adds a column
|
|
// in the front that acts as a checkbox and responds to controls if Focused.
|
|
func (m Model) SelectableRows(selectable bool) Model {
|
|
m.selectableRows = selectable
|
|
|
|
hasSelectColumn := len(m.columns) > 0 && m.columns[0].key == columnKeySelect
|
|
|
|
if hasSelectColumn != selectable {
|
|
if selectable {
|
|
m.columns = append([]Column{
|
|
NewColumn(columnKeySelect, m.selectedText, len([]rune(m.selectedText))),
|
|
}, m.columns...)
|
|
} else {
|
|
m.columns = m.columns[1:]
|
|
}
|
|
}
|
|
|
|
m.recalculateWidth()
|
|
|
|
return m
|
|
}
|
|
|
|
// HighlightedRow returns the full Row that's currently highlighted by the user.
|
|
func (m Model) HighlightedRow() Row {
|
|
if len(m.GetVisibleRows()) > 0 {
|
|
return m.GetVisibleRows()[m.rowCursorIndex]
|
|
}
|
|
|
|
// TODO: Better way to do this without pointers/nil? Or should it be nil?
|
|
return Row{}
|
|
}
|
|
|
|
// SelectedRows returns all rows that have been set as selected by the user.
|
|
func (m Model) SelectedRows() []Row {
|
|
selectedRows := []Row{}
|
|
|
|
for _, row := range m.GetVisibleRows() {
|
|
if row.selected {
|
|
selectedRows = append(selectedRows, row)
|
|
}
|
|
}
|
|
|
|
return selectedRows
|
|
}
|
|
|
|
// HighlightStyle sets a custom style to use when the row is being highlighted
|
|
// by the cursor. This should not be used with WithRowStyleFunc. Instead, use
|
|
// the IsHighlighted field in the style function.
|
|
func (m Model) HighlightStyle(style lipgloss.Style) Model {
|
|
m.highlightStyle = style
|
|
|
|
return m
|
|
}
|
|
|
|
// Focused allows the table to show highlighted rows and take in controls of
|
|
// up/down/space/etc to let the user navigate the table and interact with it.
|
|
func (m Model) Focused(focused bool) Model {
|
|
m.focused = focused
|
|
|
|
return m
|
|
}
|
|
|
|
// Filtered allows the table to show rows that match the filter.
|
|
func (m Model) Filtered(filtered bool) Model {
|
|
m.filtered = filtered
|
|
m.visibleRowCacheUpdated = false
|
|
|
|
if m.minimumHeight > 0 {
|
|
m.recalculateHeight()
|
|
}
|
|
|
|
return m
|
|
}
|
|
|
|
// StartFilterTyping focuses the text input to allow user typing to filter.
|
|
func (m Model) StartFilterTyping() Model {
|
|
m.filterTextInput.Focus()
|
|
|
|
return m
|
|
}
|
|
|
|
// WithStaticFooter adds a footer that only displays the given text.
|
|
func (m Model) WithStaticFooter(footer string) Model {
|
|
m.staticFooter = footer
|
|
|
|
if m.minimumHeight > 0 {
|
|
m.recalculateHeight()
|
|
}
|
|
|
|
return m
|
|
}
|
|
|
|
// WithPageSize enables pagination using the given page size. This can be called
|
|
// again at any point to resize the height of the table.
|
|
func (m Model) WithPageSize(pageSize int) Model {
|
|
m.pageSize = pageSize
|
|
|
|
maxPages := m.MaxPages()
|
|
|
|
if m.currentPage >= maxPages {
|
|
m.currentPage = maxPages - 1
|
|
}
|
|
|
|
if m.minimumHeight > 0 {
|
|
m.recalculateHeight()
|
|
}
|
|
|
|
return m
|
|
}
|
|
|
|
// WithNoPagination disables pagination in the table.
|
|
func (m Model) WithNoPagination() Model {
|
|
m.pageSize = 0
|
|
|
|
if m.minimumHeight > 0 {
|
|
m.recalculateHeight()
|
|
}
|
|
|
|
return m
|
|
}
|
|
|
|
// WithPaginationWrapping sets whether to wrap around from the beginning to the
|
|
// end when navigating through pages. Defaults to true.
|
|
func (m Model) WithPaginationWrapping(wrapping bool) Model {
|
|
m.paginationWrapping = wrapping
|
|
|
|
return m
|
|
}
|
|
|
|
// WithSelectedText describes what text to show when selectable rows are enabled.
|
|
// The selectable column header will use the selected text string.
|
|
func (m Model) WithSelectedText(unselected, selected string) Model {
|
|
m.selectedText = selected
|
|
m.unselectedText = unselected
|
|
|
|
if len(m.columns) > 0 && m.columns[0].key == columnKeySelect {
|
|
m.columns[0] = NewColumn(columnKeySelect, m.selectedText, len([]rune(m.selectedText)))
|
|
m.recalculateWidth()
|
|
}
|
|
|
|
return m
|
|
}
|
|
|
|
// WithBaseStyle applies a base style as the default for everything in the table.
|
|
// This is useful for border colors, default alignment, default color, etc.
|
|
func (m Model) WithBaseStyle(style lipgloss.Style) Model {
|
|
m.baseStyle = style
|
|
|
|
return m
|
|
}
|
|
|
|
// WithTargetWidth sets the total target width of the table, including borders.
|
|
// This only takes effect when using flex columns. When using flex columns,
|
|
// columns will stretch to fill out to the total width given here.
|
|
func (m Model) WithTargetWidth(totalWidth int) Model {
|
|
m.targetTotalWidth = totalWidth
|
|
|
|
m.recalculateWidth()
|
|
|
|
return m
|
|
}
|
|
|
|
// WithMinimumHeight sets the minimum total height of the table, including borders.
|
|
func (m Model) WithMinimumHeight(minimumHeight int) Model {
|
|
m.minimumHeight = minimumHeight
|
|
|
|
m.recalculateHeight()
|
|
|
|
return m
|
|
}
|
|
|
|
// PageDown goes to the next page of a paginated table, wrapping to the first
|
|
// page if the table is already on the last page.
|
|
func (m Model) PageDown() Model {
|
|
m.pageDown()
|
|
|
|
return m
|
|
}
|
|
|
|
// PageUp goes to the previous page of a paginated table, wrapping to the
|
|
// last page if the table is already on the first page.
|
|
func (m Model) PageUp() Model {
|
|
m.pageUp()
|
|
|
|
return m
|
|
}
|
|
|
|
// PageLast goes to the last page of a paginated table.
|
|
func (m Model) PageLast() Model {
|
|
m.pageLast()
|
|
|
|
return m
|
|
}
|
|
|
|
// PageFirst goes to the first page of a paginated table.
|
|
func (m Model) PageFirst() Model {
|
|
m.pageFirst()
|
|
|
|
return m
|
|
}
|
|
|
|
// WithCurrentPage sets the current page (1 as the first page) of a paginated
|
|
// table, bounded to the total number of pages. The current selected row will
|
|
// be set to the top row of the page if the page changed.
|
|
func (m Model) WithCurrentPage(currentPage int) Model {
|
|
if m.pageSize == 0 || currentPage == m.CurrentPage() {
|
|
return m
|
|
}
|
|
if currentPage < 1 {
|
|
currentPage = 1
|
|
} else {
|
|
maxPages := m.MaxPages()
|
|
|
|
if currentPage > maxPages {
|
|
currentPage = maxPages
|
|
}
|
|
}
|
|
m.currentPage = currentPage - 1
|
|
m.rowCursorIndex = m.currentPage * m.pageSize
|
|
|
|
return m
|
|
}
|
|
|
|
// WithColumns sets the visible columns for the table, so that columns can be
|
|
// added/removed/resized or headers rewritten.
|
|
func (m Model) WithColumns(columns []Column) Model {
|
|
// Deep copy to avoid edits
|
|
m.columns = make([]Column, len(columns))
|
|
copy(m.columns, columns)
|
|
|
|
m.recalculateWidth()
|
|
|
|
if m.selectableRows {
|
|
// Re-add the selectable column
|
|
m = m.SelectableRows(true)
|
|
}
|
|
|
|
return m
|
|
}
|
|
|
|
// WithFilterInput makes the table use the provided text input bubble for
|
|
// filtering rather than using the built-in default. This allows for external
|
|
// text input controls to be used.
|
|
func (m Model) WithFilterInput(input textinput.Model) Model {
|
|
if m.filterTextInput.Value() != input.Value() {
|
|
m.pageFirst()
|
|
}
|
|
|
|
m.filterTextInput = input
|
|
m.visibleRowCacheUpdated = false
|
|
|
|
return m
|
|
}
|
|
|
|
// WithFilterInputValue sets the filter value to the given string, immediately
|
|
// applying it as if the user had typed it in. Useful for external filter inputs
|
|
// that are not necessarily a text input.
|
|
func (m Model) WithFilterInputValue(value string) Model {
|
|
if m.filterTextInput.Value() != value {
|
|
m.pageFirst()
|
|
}
|
|
|
|
m.filterTextInput.SetValue(value)
|
|
m.filterTextInput.Blur()
|
|
m.visibleRowCacheUpdated = false
|
|
|
|
return m
|
|
}
|
|
|
|
// WithFilterFunc adds a filter function to the model. If the function returns
|
|
// true, the row will be included in the filtered results. If the function
|
|
// is nil, the function won't be used and instead the default filtering will be applied,
|
|
// if any.
|
|
func (m Model) WithFilterFunc(shouldInclude FilterFunc) Model {
|
|
m.filterFunc = shouldInclude
|
|
|
|
m.visibleRowCacheUpdated = false
|
|
|
|
return m
|
|
}
|
|
|
|
// WithFuzzyFilter enables fuzzy filtering for the table.
|
|
func (m Model) WithFuzzyFilter() Model {
|
|
return m.WithFilterFunc(filterFuncFuzzy)
|
|
}
|
|
|
|
// WithFooterVisibility sets the visibility of the footer.
|
|
func (m Model) WithFooterVisibility(visibility bool) Model {
|
|
m.footerVisible = visibility
|
|
|
|
if m.minimumHeight > 0 {
|
|
m.recalculateHeight()
|
|
}
|
|
|
|
return m
|
|
}
|
|
|
|
// WithHeaderVisibility sets the visibility of the header.
|
|
func (m Model) WithHeaderVisibility(visibility bool) Model {
|
|
m.headerVisible = visibility
|
|
|
|
if m.minimumHeight > 0 {
|
|
m.recalculateHeight()
|
|
}
|
|
|
|
return m
|
|
}
|
|
|
|
// WithMaxTotalWidth sets the maximum total width that the table should render.
|
|
// If this width is exceeded by either the target width or by the total width
|
|
// of all the columns (including borders!), anything extra will be treated as
|
|
// overflow and horizontal scrolling will be enabled to see the rest.
|
|
func (m Model) WithMaxTotalWidth(maxTotalWidth int) Model {
|
|
m.maxTotalWidth = maxTotalWidth
|
|
|
|
m.recalculateWidth()
|
|
|
|
return m
|
|
}
|
|
|
|
// WithHorizontalFreezeColumnCount freezes the given number of columns to the
|
|
// left side. This is useful for things like ID or Name columns that should
|
|
// always be visible even when scrolling.
|
|
func (m Model) WithHorizontalFreezeColumnCount(columnsToFreeze int) Model {
|
|
m.horizontalScrollFreezeColumnsCount = columnsToFreeze
|
|
|
|
m.recalculateWidth()
|
|
|
|
return m
|
|
}
|
|
|
|
// ScrollRight moves one column to the right. Use with WithMaxTotalWidth.
|
|
func (m Model) ScrollRight() Model {
|
|
m.scrollRight()
|
|
|
|
return m
|
|
}
|
|
|
|
// ScrollLeft moves one column to the left. Use with WithMaxTotalWidth.
|
|
func (m Model) ScrollLeft() Model {
|
|
m.scrollLeft()
|
|
|
|
return m
|
|
}
|
|
|
|
// WithMissingDataIndicator sets an indicator to use when data for a column is
|
|
// not found in a given row. Note that this is for completely missing data,
|
|
// an empty string or other zero value that is explicitly set is not considered
|
|
// to be missing.
|
|
func (m Model) WithMissingDataIndicator(str string) Model {
|
|
m.missingDataIndicator = str
|
|
|
|
return m
|
|
}
|
|
|
|
// WithMissingDataIndicatorStyled sets a styled indicator to use when data for
|
|
// a column is not found in a given row. Note that this is for completely
|
|
// missing data, an empty string or other zero value that is explicitly set is
|
|
// not considered to be missing.
|
|
func (m Model) WithMissingDataIndicatorStyled(styled StyledCell) Model {
|
|
m.missingDataIndicator = styled
|
|
|
|
return m
|
|
}
|
|
|
|
// WithAllRowsDeselected deselects any rows that are currently selected.
|
|
func (m Model) WithAllRowsDeselected() Model {
|
|
rows := m.GetVisibleRows()
|
|
|
|
for i, row := range rows {
|
|
if row.selected {
|
|
rows[i] = row.Selected(false)
|
|
}
|
|
}
|
|
|
|
m.rows = rows
|
|
|
|
return m
|
|
}
|
|
|
|
// WithMultiline sets whether or not to wrap text in cells to multiple lines.
|
|
func (m Model) WithMultiline(multiline bool) Model {
|
|
m.multiline = multiline
|
|
|
|
return m
|
|
}
|
|
|
|
// WithAdditionalShortHelpKeys enables you to add more keybindings to the 'short help' view.
|
|
func (m Model) WithAdditionalShortHelpKeys(keys []key.Binding) Model {
|
|
m.additionalShortHelpKeys = func() []key.Binding {
|
|
return keys
|
|
}
|
|
|
|
return m
|
|
}
|
|
|
|
// WithAdditionalFullHelpKeys enables you to add more keybindings to the 'full help' view.
|
|
func (m Model) WithAdditionalFullHelpKeys(keys []key.Binding) Model {
|
|
m.additionalFullHelpKeys = func() []key.Binding {
|
|
return keys
|
|
}
|
|
|
|
return m
|
|
}
|
|
|
|
// WithGlobalMetadata applies the given metadata to the table. This metadata is passed to
|
|
// some functions in FilterFuncInput and StyleFuncInput to enable more advanced decisions,
|
|
// such as setting some global theme variable to reference, etc. Has no effect otherwise.
|
|
func (m Model) WithGlobalMetadata(metadata map[string]any) Model {
|
|
m.metadata = metadata
|
|
|
|
return m
|
|
}
|