Documentation
¶
Overview ¶
Package teatable provides a generic, feature-rich terminal table widget for the Bubble Tea framework.
Quick start ¶
Implement Datasource for your item type and pass it to New:
type Employee struct { ID, Name, Department string }
type EmployeeDS struct {
teatable.InMemorySource[Employee]
}
func (d *EmployeeDS) RowId(e Employee) string { return e.ID }
func (d *EmployeeDS) CellValue(e Employee, col string) string {
switch col {
case "name": return e.Name
case "department": return e.Department
}
return ""
}
source := &EmployeeDS{}
source.SetData(employees)
cols := []teatable.Column{
teatable.NewColumn("name", "Name", teatable.WithFixedWidth(20)),
teatable.NewColumn("department", "Department", teatable.WithFlexRatio(1)),
}
// In Init(): return tbl.Refresh()
tbl := teatable.New(source, ctx, cols)
Columns ¶
Each column is created with NewColumn (or NewColumnFromTemplate) and accepts a variadic list of ColumnOption values.
## Width modes
Three mutually exclusive width modes are available:
Auto (default) — the column width is derived from the widest cell value. No extra options needed.
Fixed — the content area is exactly w characters wide regardless of cell content:
teatable.WithFixedWidth(w)
Flex — remaining horizontal space (after fixed and auto columns are measured) is distributed among flex columns in proportion to their ratio. The column never shrinks below its minimum width, which defaults to zero and can be raised with WithMinWidth:
teatable.WithFlexRatio(2) // flex weight 2 teatable.WithFlexRatio(1), teatable.WithMinWidth(10) // weight 1, min 10 chars
## Styles
Column-level styles are set with WithColumnStyles. ColumnStyles has two fields:
- Header — applied to the column header cell.
- Cell — applied to every data cell in this column.
Column cell styles are a baseline: the row style (see Row styles below) is blended on top during render.
teatable.WithColumnStyles(teatable.ColumnStyles{
Header: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("12")),
Cell: lipgloss.NewStyle().Foreground(lipgloss.Color("7")),
})
## Ellipsis
When a cell value is truncated to fit the column width, an optional suffix is appended with WithEllipsis:
teatable.WithEllipsis("…")
## Sortable columns
Mark a column as sortable with WithSortable. The datasource must also implement LocalSortable (client-side) or ServerSortable (server-side) for sorting to take effect. Pressing the sort key cycles through ascending → descending → off.
teatable.WithSortable()
## Column groups
Multiple adjacent columns can be spanned by a group header row. Create a group with NewColumnGroup and assign columns to it with WithGroup:
contacts := teatable.NewColumnGroup("Contact Details")
teatable.NewColumn("email", "Email", teatable.WithGroup(contacts))
teatable.NewColumn("phone", "Phone", teatable.WithGroup(contacts))
Groups can be nested: pass WithGroup as a ColumnGroupOption to place one group inside another.
Column templates ¶
A ColumnTemplate captures a reusable set of options that can be applied to many columns without repetition:
numericTmpl := teatable.NewColumnTemplate(
teatable.WithFixedWidth(12),
teatable.WithEllipsis("…"),
teatable.WithColumnStyles(teatable.ColumnStyles{
Header: lipgloss.NewStyle().Align(lipgloss.Right),
Cell: lipgloss.NewStyle().Align(lipgloss.Right),
}),
)
// Derive a narrower variant:
shortNumericTmpl := teatable.NewColumnTemplateFromTemplate(numericTmpl,
teatable.WithFixedWidth(6),
)
// Create columns from the template:
teatable.NewColumnFromTemplate("price", "Price", numericTmpl)
teatable.NewColumnFromTemplate("qty", "Qty", shortNumericTmpl)
NewColumnGroupFromTemplate extracts the Header style from a template and applies it to a group header, keeping column and group styling in sync.
Frozen (pinned) columns ¶
Columns at the left or right edge can be frozen so that they remain visible while the user scrolls horizontally. Pass the count to New as options:
teatable.New(ds, ctx, cols,
teatable.WithFrozenColumns(2), // pin first 2 columns on the left
teatable.WithRightFrozenColumns(1), // pin last column on the right
)
After creation the counts can be changed with Model.SetColumns (rebuild the column slice) or by calling the relevant model setters. A distinct vertical divider separates frozen columns from scrollable ones; see SimpleFrozenColumnsDivider.
Dividers ¶
Dividers are configured through TableDividers and passed via WithDividers:
teatable.New(ds, ctx, cols, teatable.WithDividers(teatable.SimpleDividers()))
Ready-made presets:
- SimpleDividers — header divider only.
- FullGridDividers — all dividers (header, rows, frozen separator, column separators).
- ASCIIDividers — ASCII characters only (suitable for non-Unicode terminals).
- StyledFullGridDividers — full grid with a uniform lipgloss.Style.
Individual divider constructors allow mixing styles:
- SimpleHeaderDivider / SimpleStyledHeaderDivider — under the header.
- SimpleRowsDivider / SimpleStyledRowsDivider — between data rows.
- SimpleColumnsDivider / SimpleStyledColumnsDivider — between regular columns.
- SimpleFrozenColumnsDivider / SimpleStyledFrozenColumnsDivider — after the last left-frozen column.
- SimpleFrozenDivider / SimpleStyledFrozenDivider — between left-frozen columns.
- SimpleRightFrozenColumnsDivider / SimpleStyledRightFrozenColumnsDivider — before the first right-frozen column.
Row styles ¶
Row appearance is driven by RowStyles, configured via WithRowStyles or Model.SetRowStyles. Six fields control the five row states:
styles := teatable.RowStyles{
Normal: lipgloss.NewStyle(),
Alternate: lipgloss.NewStyle().Faint(true),
Cursor: lipgloss.NewStyle().Reverse(true),
Selected: lipgloss.NewStyle().Bold(true),
CursorSelected: lipgloss.NewStyle().Reverse(true).Bold(true),
}
## State priority
When a row matches multiple states the following priority applies (highest first):
- CursorSelected — row is both under the cursor and selected.
- Cursor — row is under the cursor (not selected).
- Selected — row is selected (cursor is elsewhere).
- Alternate — row has an odd index and WithAlternateRows is enabled.
- Normal — default style for all other rows.
## ANSI stripping
Cell values may already contain ANSI color sequences (e.g. from a syntax highlighter). When the row style applies its own background or foreground color, the embedded sequences can interfere. Set the strip flags to remove them before the row style is applied:
styles.CursorStripANSI = true // strip in cursor state styles.SelectedStripANSI = true // strip in selected state styles.CursorSelectedStripANSI = true // strip in cursor+selected state
As a shortcut, Model.SetStripANSIForActiveStates sets all three flags at once.
## Per-row and per-cell style overrides
For dynamic styling (e.g. coloring critical rows red), install a styler callback:
model.SetRowStyler(func(row MyType, base lipgloss.Style, state teatable.RowState) lipgloss.Style {
if row.IsAlert {
return base.Foreground(lipgloss.Color("9"))
}
return base
})
model.SetCellStyler(func(row MyType, col string, base lipgloss.Style, state teatable.RowState) lipgloss.Style {
if col == "status" && row.Status == "error" {
return base.Bold(true).Foreground(lipgloss.Color("9"))
}
return base
})
Scroll indicator ¶
A vertical scroll indicator (scrollbar) can be rendered alongside the table. Configure it with ScrollIndicatorConfig and pass it via WithScrollIndicator:
teatable.New(ds, ctx, cols,
teatable.WithScrollIndicator(teatable.DataOnlyScrollIndicator()),
)
## Modes
Three ScrollIndicatorMode values control placement:
- ScrollIndicatorHidden — the indicator is not rendered (default).
- ScrollIndicatorDataOnly — the indicator spans the data rows only, excluding the header row and dividers.
- ScrollIndicatorFullHeight — the indicator spans the full table height, including the header and all dividers.
## Appearance
ScrollIndicatorConfig exposes three appearance fields:
- ThumbChar — character drawn for the thumb (current viewport position), e.g. "█".
- TrackChar — character drawn for the track background, e.g. "░".
- Style — a lipgloss.Style applied to both thumb and track.
Ready-made constructors:
- DefaultScrollIndicator — hidden indicator (no allocation).
- DataOnlyScrollIndicator — data-area indicator with "█"/"░" characters.
- FullHeightScrollIndicator — full-height indicator with "█"/"░" characters.
Custom example:
teatable.New(ds, ctx, cols,
teatable.WithScrollIndicator(teatable.ScrollIndicatorConfig{
Mode: teatable.ScrollIndicatorDataOnly,
ThumbChar: "┃",
TrackChar: "╎",
Style: lipgloss.NewStyle().Foreground(lipgloss.Color("240")),
}),
)
Index ¶
- Variables
- func StringCompare[T any](ds Datasource[T], item T, column string, other T) int
- func WithAlternateRows() func(*Options)
- func WithDividers(d TableDividers) func(*Options)
- func WithFrozenColumns(n int) func(*Options)
- func WithFrozenColumnsDivider(d VerticalDivider) func(*Options)
- func WithFrozenDivider(d VerticalDivider) func(*Options)
- func WithGroup(g ColumnGroup) groupOption
- func WithHeaderDivider(d HorizontalDivider) func(*Options)
- func WithNormalBorder() func(*Options)
- func WithOuterBorder(b OuterBorder) func(*Options)
- func WithRightFrozenColumns(n int) func(*Options)
- func WithRoundedBorder() func(*Options)
- func WithRowStyles(styles RowStyles) func(*Options)
- func WithScrollIndicator(c ScrollIndicatorConfig) func(*Options)
- func WithSelectionEnabled() func(*Options)
- func WithTitle(title string) func(*Options)
- type Align
- type BottomBorderRenderer
- type Column
- type ColumnGroup
- type ColumnGroupOption
- type ColumnGroupStyles
- type ColumnOption
- type ColumnStyles
- type ColumnTemplate
- type CursorMovedMsg
- type DataLoadedMsg
- type Datasource
- type FilteredRowsChangedMsg
- type HorizontalDivider
- type InMemorySource
- type KeyMap
- type LoadErrorMsg
- type Loader
- type LocalFilterable
- type LocalSortable
- type Model
- func (m *Model[T]) AlternateRowsEnabled() bool
- func (m *Model[T]) ClearSelection() tea.Cmd
- func (m *Model[T]) ClearSort() tea.Cmd
- func (m *Model[T]) Columns() []Column
- func (m *Model[T]) CurrentPage() int
- func (m *Model[T]) CursorIndex() (int, bool)
- func (m *Model[T]) CursorRow() (T, bool)
- func (m *Model[T]) Dividers() TableDividers
- func (m *Model[T]) Filter() string
- func (m *Model[T]) FilteredRow(index int) (T, bool)
- func (m *Model[T]) FilteredRowIdIndex(id string) (int, bool)
- func (m *Model[T]) FilteredRowIndex(row T) (int, bool)
- func (m *Model[T]) FilteredRows() []T
- func (m *Model[T]) FilteredRowsCount() int
- func (m *Model[T]) FrozenColumns() int
- func (m *Model[T]) HScrollOffset() int
- func (m *Model[T]) HasMore() bool
- func (m *Model[T]) IsRowSelected(filteredIndex int) bool
- func (m *Model[T]) KeyMap() KeyMap
- func (m *Model[T]) Loading() bool
- func (m *Model[T]) MoveCursorDown() tea.Cmd
- func (m *Model[T]) MoveCursorPageDown() tea.Cmd
- func (m *Model[T]) MoveCursorPageUp() tea.Cmd
- func (m *Model[T]) MoveCursorToFirstRow() tea.Cmd
- func (m *Model[T]) MoveCursorToLastRow() tea.Cmd
- func (m *Model[T]) MoveCursorToRow(index int) tea.Cmd
- func (m *Model[T]) MoveCursorUp() tea.Cmd
- func (m *Model[T]) NextPage() tea.Cmd
- func (m *Model[T]) OuterBorder() OuterBorder
- func (m *Model[T]) PrevPage() tea.Cmd
- func (m *Model[T]) Refresh() tea.Cmd
- func (m *Model[T]) Reload() tea.Cmd
- func (m *Model[T]) RightFrozenColumns() int
- func (m *Model[T]) Row(index int) (T, bool)
- func (m *Model[T]) RowIdIndex(id string) (int, bool)
- func (m *Model[T]) RowIndex(row T) (int, bool)
- func (m *Model[T]) RowStyles() RowStyles
- func (m *Model[T]) Rows() []T
- func (m *Model[T]) RowsCount() int
- func (m *Model[T]) ScrollIndicator() ScrollIndicatorConfig
- func (m *Model[T]) ScrollLeft()
- func (m *Model[T]) ScrollRight()
- func (m *Model[T]) SelectAllRows() tea.Cmd
- func (m *Model[T]) SelectRow(filteredIndex int) (tea.Cmd, error)
- func (m *Model[T]) SelectedRow(index int) (T, bool)
- func (m *Model[T]) SelectedRows() []T
- func (m *Model[T]) SelectedRowsCount() int
- func (m *Model[T]) SelectionEnabled() bool
- func (m *Model[T]) SetAlternateRowsEnabled(enabled bool)
- func (m *Model[T]) SetBottomBorderRenderer(fn BottomBorderRenderer[T], align Align, offset int)
- func (m *Model[T]) SetCellStyler(s func(T, string, lipgloss.Style, RowState) lipgloss.Style)
- func (m *Model[T]) SetColumns(columns []Column)
- func (m *Model[T]) SetDividers(d TableDividers)
- func (m *Model[T]) SetFilter(filter string) tea.Cmd
- func (m *Model[T]) SetFrozenColumns(n int)
- func (m *Model[T]) SetHeight(height int)
- func (m *Model[T]) SetKeyMap(keyMap KeyMap)
- func (m *Model[T]) SetOuterBorder(b OuterBorder)
- func (m *Model[T]) SetRightFrozenColumns(n int)
- func (m *Model[T]) SetRowStyler(s func(T, lipgloss.Style, RowState) lipgloss.Style)
- func (m *Model[T]) SetRowStyles(styles RowStyles)
- func (m *Model[T]) SetScrollIndicator(c ScrollIndicatorConfig)
- func (m *Model[T]) SetSelectionEnabled(enabled bool)
- func (m *Model[T]) SetShowHeader(show bool)
- func (m *Model[T]) SetSort(columnName string, direction SortDirection) (tea.Cmd, error)
- func (m *Model[T]) SetStripANSIForActiveStates(strip bool)
- func (m *Model[T]) SetTitle(title string)
- func (m *Model[T]) SetWidth(width int)
- func (m *Model[T]) ShowHeader() bool
- func (m *Model[T]) SortActive() bool
- func (m *Model[T]) SortColumn() string
- func (m *Model[T]) SortDirection() SortDirection
- func (m *Model[T]) Title() string
- func (m *Model[T]) ToggleRowSelection(filteredIndex int) (tea.Cmd, error)
- func (m *Model[T]) ToggleSort(columnName string) (tea.Cmd, error)
- func (m *Model[T]) TotalCount() int
- func (m *Model[T]) UnselectRow(filteredIndex int) (tea.Cmd, error)
- func (m *Model[T]) Update(msg tea.Msg) tea.Cmd
- func (m *Model[T]) View() string
- type MoreLoadedMsg
- type Options
- type OuterBorder
- type PageLoadedMsg
- type Pager
- type RowState
- type RowStyles
- type ScrollIndicatorConfig
- type ScrollIndicatorMode
- type SelectionChangedMsg
- type ServerFilterable
- type ServerSortable
- type SortDirection
- type Streamer
- type TableDividers
- type VerticalDivider
- func SimpleColumnsDivider() VerticalDivider
- func SimpleFrozenColumnsDivider() VerticalDivider
- func SimpleFrozenDivider() VerticalDivider
- func SimpleRightFrozenColumnsDivider() VerticalDivider
- func SimpleStyledColumnsDivider(style lipgloss.Style) VerticalDivider
- func SimpleStyledFrozenColumnsDivider(style lipgloss.Style) VerticalDivider
- func SimpleStyledFrozenDivider(style lipgloss.Style) VerticalDivider
- func SimpleStyledRightFrozenColumnsDivider(style lipgloss.Style) VerticalDivider
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ErrColumnNotSortable = errors.New("column is not sortable")
ErrColumnNotSortable is returned by sort methods when the column is not marked as sortable.
var ErrIndexOutOfRange = errors.New("index out of range")
ErrIndexOutOfRange is returned when the given index is out of the filtered row list bounds.
var ErrSelectionDisabled = errors.New("selection disabled")
ErrSelectionDisabled is returned by selection methods when row selection is disabled.
Functions ¶
func StringCompare ¶
func StringCompare[T any](ds Datasource[T], item T, column string, other T) int
StringCompare compares two items by column using strings.Compare on their CellValues. Intended for trivial LocalSortable implementations:
func (ds MyDatasource) Compare(item MyType, col string, other MyType) int {
return teatable.StringCompare(ds, item, col, other)
}
Example ¶
ExampleStringCompare shows how to implement [tt.LocalSortable.Compare] by delegating to StringCompare for lexicographic column comparison.
package main
import (
"fmt"
"strings"
tt "github.com/anadale/teaservice/teatable"
)
// product is the domain type used in teatable examples.
type product struct {
ID string
Name string
Category string
Price string
}
// productDS implements [tt.Datasource][product], [tt.LocalSortable][product],
// and [tt.LocalFilterable][product] using the embedded [tt.InMemorySource].
type productDS struct {
tt.InMemorySource[product]
}
func (d *productDS) RowId(p product) string { return p.ID }
func (d *productDS) CellValue(p product, col string) string {
switch col {
case "Name":
return p.Name
case "Category":
return p.Category
case "Price":
return p.Price
}
return ""
}
func (d *productDS) Compare(item product, col string, other product) int {
return tt.StringCompare(d, item, col, other)
}
func (d *productDS) Matches(item product, filter string) bool {
return strings.Contains(item.Name, filter) || strings.Contains(item.Category, filter)
}
func sampleProducts() []product {
return []product{
{"1", "Widget A", "Widgets", "$9.99"},
{"2", "Gadget Pro", "Gadgets", "$49.99"},
{"3", "Widget B", "Widgets", "$12.99"},
{"4", "Gadget Lite", "Gadgets", "$19.99"},
}
}
func main() {
src := &productDS{}
src.SetData(sampleProducts())
a := product{Name: "Alpha"}
b := product{Name: "Beta"}
result := tt.StringCompare(src, a, "Name", b)
fmt.Println("Alpha < Beta:", result < 0)
}
Output: Alpha < Beta: true
func WithAlternateRows ¶
func WithAlternateRows() func(*Options)
WithAlternateRows enables alternating row styles.
func WithDividers ¶
func WithDividers(d TableDividers) func(*Options)
WithDividers sets the table dividers.
func WithFrozenColumns ¶
WithFrozenColumns sets the number of frozen columns on the left.
func WithFrozenColumnsDivider ¶
func WithFrozenColumnsDivider(d VerticalDivider) func(*Options)
WithFrozenColumnsDivider sets the vertical divider after the last frozen column.
func WithFrozenDivider ¶
func WithFrozenDivider(d VerticalDivider) func(*Options)
WithFrozenDivider sets the vertical divider between frozen columns.
func WithGroup ¶
func WithGroup(g ColumnGroup) groupOption
WithGroup returns an option that assigns the column to the given group, or nests a group inside the given parent group.
func WithHeaderDivider ¶
func WithHeaderDivider(d HorizontalDivider) func(*Options)
WithHeaderDivider sets the horizontal divider under the header.
func WithNormalBorder ¶
func WithNormalBorder() func(*Options)
WithNormalBorder sets a standard rectangular border.
func WithOuterBorder ¶
func WithOuterBorder(b OuterBorder) func(*Options)
WithOuterBorder sets the table outer border.
func WithRightFrozenColumns ¶
WithRightFrozenColumns sets the number of frozen columns on the right.
func WithRoundedBorder ¶
func WithRoundedBorder() func(*Options)
WithRoundedBorder sets a rounded border.
func WithRowStyles ¶
WithRowStyles sets the row styles for the table.
func WithScrollIndicator ¶
func WithScrollIndicator(c ScrollIndicatorConfig) func(*Options)
WithScrollIndicator sets the scroll indicator configuration.
func WithSelectionEnabled ¶
func WithSelectionEnabled() func(*Options)
WithSelectionEnabled enables row selection.
Types ¶
type Align ¶
type Align int
Align controls horizontal placement of content embedded in the bottom border.
type BottomBorderRenderer ¶
BottomBorderRenderer is a function that produces a string to embed in the bottom border. It receives the current model and returns the content (may include ANSI styling). The visual width of the returned string must not exceed the table's inner width.
func PagerStatus ¶
func PagerStatus[T any]() BottomBorderRenderer[T]
PagerStatus returns a BottomBorderRenderer that shows current page navigation: "← 1 2 … [5] … 99 →", with the current page surrounded by brackets. Returns an empty string when totalCount or pageSize is not set (≤ 0).
func StreamerStatus ¶
func StreamerStatus[T any]() BottomBorderRenderer[T]
StreamerStatus returns a BottomBorderRenderer that shows "↓ more" when more streamed data is available, or an empty string when all data has been loaded.
type Column ¶
type Column struct {
// Name is the unique column name, used in Datasource.CellValue(item, columnName) and LocalSortable.Compare(item, columnName, other).
Name string
// Title is the displayed column header.
Title string
// contains filtered or unexported fields
}
Column describes a single table column: its name, title, width mode, styles, and sort flag.
func NewColumn ¶
func NewColumn(name, title string, opts ...ColumnOption) Column
NewColumn creates a column with the given name, title, and options. Panics if the name is empty.
Example ¶
ExampleNewColumn shows column creation with various sizing options.
package main
import (
"fmt"
tt "github.com/anadale/teaservice/teatable"
)
func main() {
// Fixed-width column — exact character width.
id := tt.NewColumn("id", "ID", tt.WithFixedWidth(6))
// Proportional columns — distribute remaining space by ratio.
name := tt.NewColumn("name", "Name", tt.WithFlexRatio(2))
dept := tt.NewColumn("dept", "Department", tt.WithFlexRatio(1))
// Sortable column — enables SetSort and ToggleSort for this column.
score := tt.NewColumn("score", "Score", tt.WithFlexRatio(1), tt.WithSortable())
_ = []tt.Column{id, name, dept, score}
fmt.Println("columns created")
}
Output: columns created
func NewColumnFromTemplate ¶
func NewColumnFromTemplate(name, title string, tmpl ColumnTemplate, opts ...ColumnOption) Column
NewColumnFromTemplate creates a Column by applying tmpl options first, then opts.
type ColumnGroup ¶
type ColumnGroup struct {
Title string
// contains filtered or unexported fields
}
ColumnGroup represents a named group spanning one or more adjacent columns.
func NewColumnGroup ¶
func NewColumnGroup(title string, opts ...ColumnGroupOption) ColumnGroup
NewColumnGroup creates a ColumnGroup with the given title and options.
func NewColumnGroupFromTemplate ¶
func NewColumnGroupFromTemplate(title string, tmpl ColumnTemplate, opts ...ColumnGroupOption) ColumnGroup
NewColumnGroupFromTemplate creates a ColumnGroup by extracting styles from the template (via a temporary Column) and passing them to NewColumnGroup.
type ColumnGroupOption ¶
type ColumnGroupOption interface {
// contains filtered or unexported methods
}
ColumnGroupOption is an option for configuring a ColumnGroup.
func WithGroupStyles ¶
func WithGroupStyles(styles ColumnGroupStyles) ColumnGroupOption
WithGroupStyles sets the header style for the column group.
type ColumnGroupStyles ¶
ColumnGroupStyles defines styles for the group header row.
func DefaultColumnGroupStyles ¶
func DefaultColumnGroupStyles() ColumnGroupStyles
DefaultColumnGroupStyles returns default group styles (empty styles).
type ColumnOption ¶
type ColumnOption interface {
// contains filtered or unexported methods
}
ColumnOption is an option for configuring a Column.
func WithColumnStyles ¶
func WithColumnStyles(styles ColumnStyles) ColumnOption
WithColumnStyles sets the column header and cell styles.
func WithEllipsis ¶
func WithEllipsis(s string) ColumnOption
WithEllipsis sets the string appended to truncated cell and header content (e.g. "...", "…").
func WithFixedWidth ¶
func WithFixedWidth(w int) ColumnOption
WithFixedWidth sets a fixed content width for the column in characters.
func WithFlexRatio ¶
func WithFlexRatio(ratio int) ColumnOption
WithFlexRatio sets proportional width mode with the given ratio. Free space is distributed among flex columns proportionally to their ratio. The remainder goes to the column with the largest flexRatio.
func WithMinWidth ¶
func WithMinWidth(w int) ColumnOption
WithMinWidth sets the minimum content width for a column with WithFlexRatio. On horizontal overflow, flex columns are not shrunk below this value.
func WithSortable ¶
func WithSortable() ColumnOption
WithSortable marks the column as sortable. When sorting is active, the model calls row.Compare(col.Name, other).
type ColumnStyles ¶
type ColumnStyles struct {
// Header is the style for the column header cell.
Header lipgloss.Style
// Cell is the style for the column data cells.
Cell lipgloss.Style
}
ColumnStyles defines styles for the column header and cells.
func DefaultColumnStyles ¶
func DefaultColumnStyles() ColumnStyles
DefaultColumnStyles returns default column styles (empty styles).
type ColumnTemplate ¶
type ColumnTemplate struct {
// contains filtered or unexported fields
}
ColumnTemplate holds a reusable set of ColumnOptions.
func NewColumnTemplate ¶
func NewColumnTemplate(opts ...ColumnOption) ColumnTemplate
NewColumnTemplate creates a ColumnTemplate from the given options.
func NewColumnTemplateFromTemplate ¶
func NewColumnTemplateFromTemplate(base ColumnTemplate, opts ...ColumnOption) ColumnTemplate
NewColumnTemplateFromTemplate creates a new template that applies base options first, then the provided opts.
type CursorMovedMsg ¶
type CursorMovedMsg struct {
Index int // index in filteredRows; -1 if no rows
RowId string // RowId at cursor; "" if no rows
}
CursorMovedMsg emitted when cursor position changes.
type DataLoadedMsg ¶
type DataLoadedMsg[T any] struct { Items []T }
DataLoadedMsg is emitted after a successful Loader.Load call (inMemory mode).
type Datasource ¶
type Datasource[T any] interface { // RowId returns a unique, stable identifier for the item. // Used for cursor tracking, selection, and Refresh merge-update. RowId(item T) string // CellValue returns the string representation of the field named column // for the given item. Return an empty string for unknown column names. CellValue(item T, column string) string }
Datasource is the base interface for all table data sources. T is the domain item type displayed in each row. Every datasource must implement RowId and CellValue; loading mode is declared by implementing exactly one of Loader, Pager, or Streamer.
type FilteredRowsChangedMsg ¶
type FilteredRowsChangedMsg struct {
Count int // filtered rows count
Total int // total rows count (before filter)
}
FilteredRowsChangedMsg emitted when filtered rows count changes.
type HorizontalDivider ¶
type HorizontalDivider struct {
// Enabled turns on drawing the divider.
Enabled bool
// Char is the main character of the divider line.
Char string
// LeftJoin is the character joining the left border (e.g. ├).
LeftJoin string
// RightJoin is the character joining the right border (e.g. ┤).
RightJoin string
// Crossing is the character at vertical divider crossings (e.g. ┼).
Crossing string
// CrossingDown is the character where the horizontal divider meets a vertical line
// that goes only downward (no line above). E.g. ┬. Falls back to Crossing if empty.
CrossingDown string
// Style sets the divider style.
Style lipgloss.Style
}
HorizontalDivider defines a horizontal divider (between rows or under the header).
func SimpleHeaderDivider ¶
func SimpleHeaderDivider() HorizontalDivider
SimpleHeaderDivider returns a horizontal divider under the header.
func SimpleRowsDivider ¶
func SimpleRowsDivider() HorizontalDivider
SimpleRowsDivider returns a horizontal divider between data rows.
func SimpleStyledHeaderDivider ¶
func SimpleStyledHeaderDivider(style lipgloss.Style) HorizontalDivider
SimpleStyledHeaderDivider returns a horizontal divider under the header with the specified style.
func SimpleStyledRowsDivider ¶
func SimpleStyledRowsDivider(style lipgloss.Style) HorizontalDivider
SimpleStyledRowsDivider returns a horizontal divider between data rows with the specified style.
type InMemorySource ¶
type InMemorySource[T any] struct { // contains filtered or unexported fields }
InMemorySource is an embeddable struct that implements Loader[T]. Embed it in a custom datasource type and implement [Datasource.RowId] and [Datasource.CellValue] to complete the datasource contract.
type employeeDS struct {
teatable.InMemorySource[Employee]
}
func (d *employeeDS) RowId(e Employee) string { return e.ID }
func (d *employeeDS) CellValue(e Employee, col string) string { ... }
Call InMemorySource.SetData to update the contents, then call Model.Refresh on the table to reload and merge-update with preserved cursor and scroll state.
func (*InMemorySource[T]) Data ¶
func (s *InMemorySource[T]) Data() []T
Data returns the current items slice.
func (*InMemorySource[T]) Load ¶
func (s *InMemorySource[T]) Load(_ context.Context) ([]T, error)
Load implements Loader[T] and returns a copy of the current items. A copy is returned so that once the loading goroutine has captured the slice, a subsequent InMemorySource.SetData call cannot mutate the elements it is working with. Safe when InMemorySource.SetData is called before Model.Refresh within the same BubbleTea update cycle; calling SetData from a separate goroutine without external synchronisation is a data race.
func (*InMemorySource[T]) SetData ¶
func (s *InMemorySource[T]) SetData(items []T)
SetData replaces the items slice.
type KeyMap ¶
type KeyMap struct {
// LineUp moves the cursor one row up.
LineUp key.Binding
// LineDown moves the cursor one row down.
LineDown key.Binding
// PageUp moves the cursor one page up.
PageUp key.Binding
// PageDown moves the cursor one page down.
PageDown key.Binding
// GotoTop moves the cursor to the first row and resets the horizontal scroll.
GotoTop key.Binding
// GotoBottom moves the cursor to the last row and resets the horizontal scroll.
GotoBottom key.Binding
// ScrollLeft scrolls the table horizontally left.
ScrollLeft key.Binding
// ScrollRight scrolls the table horizontally right.
ScrollRight key.Binding
// SelectAll selects all filtered rows.
SelectAll key.Binding
// DeselectAll deselects all selected rows.
DeselectAll key.Binding
// ToggleSelection toggles the selection of the row under the cursor.
ToggleSelection key.Binding
// ToggleSelectionAndLineDown toggles the selection of the current row and moves the cursor one row down.
ToggleSelectionAndLineDown key.Binding
}
KeyMap defines key bindings for table control.
func DefaultKeyMap ¶
func DefaultKeyMap() KeyMap
DefaultKeyMap returns standard key bindings: up/k, down/j, pgup/b, pgdn/f, home/g, end/G, left/h, right/l, ctrl+a, space.
type LoadErrorMsg ¶
type LoadErrorMsg struct {
Err error
}
LoadErrorMsg is emitted when a datasource load fails.
type LocalFilterable ¶
LocalFilterable enables client-side row filtering.
type LocalSortable ¶
LocalSortable enables client-side row sorting. Compare returns a negative number, zero, or positive — the same as strings.Compare.
type Model ¶
type Model[T any] struct { // contains filtered or unexported fields }
Model is the main TeaTable model. T is the domain item type displayed in each row.
func New ¶
func New[T any](ds Datasource[T], ctx context.Context, columns []Column, opts ...func(*Options)) *Model[T]
New creates a new table model with the given datasource, context, columns and options. Defaults: showHeader = true, cursor = -1 when there are no rows.
Example ¶
ExampleNew demonstrates creating an in-memory table with [tt.InMemorySource]. In a real program, call [tt.Model.SetWidth] and [tt.Model.SetHeight] from your model's Update in response to tea.WindowSizeMsg, then call [tt.Model.Refresh] from Init to trigger the first data load.
package main
import (
"context"
"fmt"
"strings"
tt "github.com/anadale/teaservice/teatable"
)
// product is the domain type used in teatable examples.
type product struct {
ID string
Name string
Category string
Price string
}
// productDS implements [tt.Datasource][product], [tt.LocalSortable][product],
// and [tt.LocalFilterable][product] using the embedded [tt.InMemorySource].
type productDS struct {
tt.InMemorySource[product]
}
func (d *productDS) RowId(p product) string { return p.ID }
func (d *productDS) CellValue(p product, col string) string {
switch col {
case "Name":
return p.Name
case "Category":
return p.Category
case "Price":
return p.Price
}
return ""
}
func (d *productDS) Compare(item product, col string, other product) int {
return tt.StringCompare(d, item, col, other)
}
func (d *productDS) Matches(item product, filter string) bool {
return strings.Contains(item.Name, filter) || strings.Contains(item.Category, filter)
}
func sampleProducts() []product {
return []product{
{"1", "Widget A", "Widgets", "$9.99"},
{"2", "Gadget Pro", "Gadgets", "$49.99"},
{"3", "Widget B", "Widgets", "$12.99"},
{"4", "Gadget Lite", "Gadgets", "$19.99"},
}
}
// loadTable runs the initial data load synchronously (for use in examples).
func loadTable[T any](tbl *tt.Model[T]) {
cmd := tbl.Refresh()
if cmd != nil {
tbl.Update(cmd())
}
}
func main() {
src := &productDS{}
src.SetData(sampleProducts())
cols := []tt.Column{
tt.NewColumn("Name", "Name", tt.WithFlexRatio(2)),
tt.NewColumn("Category", "Category", tt.WithFlexRatio(1)),
tt.NewColumn("Price", "Price", tt.WithFixedWidth(10)),
}
tbl := tt.New(src, context.Background(), cols,
tt.WithRoundedBorder(),
tt.WithTitle("Products"),
tt.WithAlternateRows(),
)
tbl.SetWidth(60)
tbl.SetHeight(10)
loadTable(tbl)
fmt.Println("rows:", tbl.RowsCount())
}
Output: rows: 4
Example (WithFilter) ¶
ExampleNew_withFilter demonstrates a table with client-side row filtering. The datasource must implement [tt.LocalFilterable].
package main
import (
"context"
"fmt"
"strings"
tt "github.com/anadale/teaservice/teatable"
)
// product is the domain type used in teatable examples.
type product struct {
ID string
Name string
Category string
Price string
}
// productDS implements [tt.Datasource][product], [tt.LocalSortable][product],
// and [tt.LocalFilterable][product] using the embedded [tt.InMemorySource].
type productDS struct {
tt.InMemorySource[product]
}
func (d *productDS) RowId(p product) string { return p.ID }
func (d *productDS) CellValue(p product, col string) string {
switch col {
case "Name":
return p.Name
case "Category":
return p.Category
case "Price":
return p.Price
}
return ""
}
func (d *productDS) Compare(item product, col string, other product) int {
return tt.StringCompare(d, item, col, other)
}
func (d *productDS) Matches(item product, filter string) bool {
return strings.Contains(item.Name, filter) || strings.Contains(item.Category, filter)
}
func sampleProducts() []product {
return []product{
{"1", "Widget A", "Widgets", "$9.99"},
{"2", "Gadget Pro", "Gadgets", "$49.99"},
{"3", "Widget B", "Widgets", "$12.99"},
{"4", "Gadget Lite", "Gadgets", "$19.99"},
}
}
// loadTable runs the initial data load synchronously (for use in examples).
func loadTable[T any](tbl *tt.Model[T]) {
cmd := tbl.Refresh()
if cmd != nil {
tbl.Update(cmd())
}
}
func main() {
src := &productDS{}
src.SetData(sampleProducts())
cols := []tt.Column{
tt.NewColumn("Name", "Name", tt.WithFlexRatio(1)),
tt.NewColumn("Category", "Category", tt.WithFlexRatio(1)),
}
tbl := tt.New(src, context.Background(), cols)
tbl.SetWidth(60)
tbl.SetHeight(10)
loadTable(tbl)
_ = tbl.SetFilter("Gadget")
fmt.Printf("filter=%q filtered=%d total=%d\n",
tbl.Filter(), tbl.FilteredRowsCount(), tbl.RowsCount())
}
Output: filter="Gadget" filtered=2 total=4
Example (WithSelection) ¶
ExampleNew_withSelection demonstrates a table with row selection enabled.
package main
import (
"context"
"fmt"
"strings"
tt "github.com/anadale/teaservice/teatable"
)
// product is the domain type used in teatable examples.
type product struct {
ID string
Name string
Category string
Price string
}
// productDS implements [tt.Datasource][product], [tt.LocalSortable][product],
// and [tt.LocalFilterable][product] using the embedded [tt.InMemorySource].
type productDS struct {
tt.InMemorySource[product]
}
func (d *productDS) RowId(p product) string { return p.ID }
func (d *productDS) CellValue(p product, col string) string {
switch col {
case "Name":
return p.Name
case "Category":
return p.Category
case "Price":
return p.Price
}
return ""
}
func (d *productDS) Compare(item product, col string, other product) int {
return tt.StringCompare(d, item, col, other)
}
func (d *productDS) Matches(item product, filter string) bool {
return strings.Contains(item.Name, filter) || strings.Contains(item.Category, filter)
}
func sampleProducts() []product {
return []product{
{"1", "Widget A", "Widgets", "$9.99"},
{"2", "Gadget Pro", "Gadgets", "$49.99"},
{"3", "Widget B", "Widgets", "$12.99"},
{"4", "Gadget Lite", "Gadgets", "$19.99"},
}
}
// loadTable runs the initial data load synchronously (for use in examples).
func loadTable[T any](tbl *tt.Model[T]) {
cmd := tbl.Refresh()
if cmd != nil {
tbl.Update(cmd())
}
}
func main() {
src := &productDS{}
src.SetData(sampleProducts())
tbl := tt.New(src, context.Background(), []tt.Column{
tt.NewColumn("Name", "Name", tt.WithFlexRatio(1)),
}, tt.WithSelectionEnabled())
tbl.SetWidth(60)
tbl.SetHeight(10)
loadTable(tbl)
_, _ = tbl.SelectRow(0)
_, _ = tbl.SelectRow(2)
fmt.Println("selected:", tbl.SelectedRowsCount())
}
Output: selected: 2
Example (WithSorting) ¶
ExampleNew_withSorting demonstrates a table with client-side sorting. The datasource must implement [tt.LocalSortable]; columns must be marked with [tt.WithSortable]. Use [tt.StringCompare] for lexicographic comparison.
package main
import (
"context"
"fmt"
"strings"
tt "github.com/anadale/teaservice/teatable"
)
// product is the domain type used in teatable examples.
type product struct {
ID string
Name string
Category string
Price string
}
// productDS implements [tt.Datasource][product], [tt.LocalSortable][product],
// and [tt.LocalFilterable][product] using the embedded [tt.InMemorySource].
type productDS struct {
tt.InMemorySource[product]
}
func (d *productDS) RowId(p product) string { return p.ID }
func (d *productDS) CellValue(p product, col string) string {
switch col {
case "Name":
return p.Name
case "Category":
return p.Category
case "Price":
return p.Price
}
return ""
}
func (d *productDS) Compare(item product, col string, other product) int {
return tt.StringCompare(d, item, col, other)
}
func (d *productDS) Matches(item product, filter string) bool {
return strings.Contains(item.Name, filter) || strings.Contains(item.Category, filter)
}
func sampleProducts() []product {
return []product{
{"1", "Widget A", "Widgets", "$9.99"},
{"2", "Gadget Pro", "Gadgets", "$49.99"},
{"3", "Widget B", "Widgets", "$12.99"},
{"4", "Gadget Lite", "Gadgets", "$19.99"},
}
}
// loadTable runs the initial data load synchronously (for use in examples).
func loadTable[T any](tbl *tt.Model[T]) {
cmd := tbl.Refresh()
if cmd != nil {
tbl.Update(cmd())
}
}
func main() {
src := &productDS{}
src.SetData(sampleProducts())
cols := []tt.Column{
tt.NewColumn("Name", "Name", tt.WithFlexRatio(1), tt.WithSortable()),
tt.NewColumn("Category", "Category", tt.WithFlexRatio(1), tt.WithSortable()),
}
tbl := tt.New(src, context.Background(), cols, tt.WithRoundedBorder())
tbl.SetWidth(60)
tbl.SetHeight(10)
loadTable(tbl)
_, err := tbl.SetSort("Name", tt.SortAsc)
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Printf("active=%v column=%s\n", tbl.SortActive(), tbl.SortColumn())
}
Output: active=true column=Name
func (*Model[T]) AlternateRowsEnabled ¶
AlternateRowsEnabled returns whether alternating rows is enabled.
func (*Model[T]) ClearSelection ¶
ClearSelection deselects all rows.
func (*Model[T]) CurrentPage ¶
CurrentPage returns the current page index (0-based) in paged mode.
func (*Model[T]) CursorIndex ¶
CursorIndex returns the cursor index in filteredRows. false if there are no rows.
func (*Model[T]) CursorRow ¶
CursorRow returns the row under the cursor. false if there are no rows.
func (*Model[T]) Dividers ¶
func (m *Model[T]) Dividers() TableDividers
Dividers return the current dividers.
func (*Model[T]) FilteredRow ¶
FilteredRow returns the row at index in the filtered set.
func (*Model[T]) FilteredRowIdIndex ¶
FilteredRowIdIndex returns the row index by RowId in the filtered set.
func (*Model[T]) FilteredRowIndex ¶
FilteredRowIndex returns the row index in the filtered set.
func (*Model[T]) FilteredRows ¶
func (m *Model[T]) FilteredRows() []T
FilteredRows returns the filtered rows.
func (*Model[T]) FilteredRowsCount ¶
FilteredRowsCount returns the number of filtered rows.
func (*Model[T]) FrozenColumns ¶
FrozenColumns returns the number of frozen columns.
func (*Model[T]) HScrollOffset ¶
HScrollOffset returns the current horizontal scroll offset.
func (*Model[T]) IsRowSelected ¶
IsRowSelected returns true if the row at index in filteredRows is selected.
func (*Model[T]) Loading ¶
Loading reports whether a data fetch is in progress. Use this to display a loading indicator in your UI.
Example ¶
ExampleModel_Loading shows that Loading reports false when no fetch is in progress.
package main
import (
"context"
"fmt"
"strings"
tt "github.com/anadale/teaservice/teatable"
)
// product is the domain type used in teatable examples.
type product struct {
ID string
Name string
Category string
Price string
}
// productDS implements [tt.Datasource][product], [tt.LocalSortable][product],
// and [tt.LocalFilterable][product] using the embedded [tt.InMemorySource].
type productDS struct {
tt.InMemorySource[product]
}
func (d *productDS) RowId(p product) string { return p.ID }
func (d *productDS) CellValue(p product, col string) string {
switch col {
case "Name":
return p.Name
case "Category":
return p.Category
case "Price":
return p.Price
}
return ""
}
func (d *productDS) Compare(item product, col string, other product) int {
return tt.StringCompare(d, item, col, other)
}
func (d *productDS) Matches(item product, filter string) bool {
return strings.Contains(item.Name, filter) || strings.Contains(item.Category, filter)
}
// loadTable runs the initial data load synchronously (for use in examples).
func loadTable[T any](tbl *tt.Model[T]) {
cmd := tbl.Refresh()
if cmd != nil {
tbl.Update(cmd())
}
}
func main() {
src := &productDS{}
tbl := tt.New(src, context.Background(), []tt.Column{
tt.NewColumn("Name", "Name", tt.WithFlexRatio(1)),
})
fmt.Println("before load:", tbl.Loading())
loadTable(tbl)
fmt.Println("after load:", tbl.Loading())
}
Output: before load: false after load: false
func (*Model[T]) MoveCursorDown ¶
MoveCursorDown moves the cursor one row down. In stream mode, automatically triggers loading more items when the last row is reached. Returns CursorMovedMsg if position changed, nil otherwise.
func (*Model[T]) MoveCursorPageDown ¶
MoveCursorPageDown moves the cursor one page down. Returns CursorMovedMsg if position changed, nil otherwise.
func (*Model[T]) MoveCursorPageUp ¶
MoveCursorPageUp moves the cursor one page up. Returns CursorMovedMsg if position changed, nil otherwise.
func (*Model[T]) MoveCursorToFirstRow ¶
MoveCursorToFirstRow moves the cursor to the first row and resets the horizontal scroll. Returns CursorMovedMsg if position changed, nil otherwise.
func (*Model[T]) MoveCursorToLastRow ¶
MoveCursorToLastRow moves the cursor to the last row and resets the horizontal scroll. Returns CursorMovedMsg if position changed, nil otherwise.
func (*Model[T]) MoveCursorToRow ¶
MoveCursorToRow moves the cursor to the row at the given index in filteredRows. Returns CursorMovedMsg if position changed, nil otherwise.
func (*Model[T]) MoveCursorUp ¶
MoveCursorUp moves the cursor one row up. Returns CursorMovedMsg if position changed, nil otherwise.
func (*Model[T]) NextPage ¶
NextPage loads the next page in paged mode. No-op if not in paged mode or on the last page.
func (*Model[T]) OuterBorder ¶
func (m *Model[T]) OuterBorder() OuterBorder
OuterBorder returns the current outer border.
func (*Model[T]) PrevPage ¶
PrevPage loads the previous page in paged mode. No-op if not in paged mode or on the first page.
func (*Model[T]) Refresh ¶
Refresh reloads data from the datasource. In in-memory mode cursor, selection, and hScrollOffset are preserved (merge semantics). In paged and stream modes a full reset is performed (page 0, cursor reset).
Example ¶
ExampleModel_Refresh demonstrates updating table data with cursor preserved.
package main
import (
"context"
"fmt"
"strings"
tt "github.com/anadale/teaservice/teatable"
)
// product is the domain type used in teatable examples.
type product struct {
ID string
Name string
Category string
Price string
}
// productDS implements [tt.Datasource][product], [tt.LocalSortable][product],
// and [tt.LocalFilterable][product] using the embedded [tt.InMemorySource].
type productDS struct {
tt.InMemorySource[product]
}
func (d *productDS) RowId(p product) string { return p.ID }
func (d *productDS) CellValue(p product, col string) string {
switch col {
case "Name":
return p.Name
case "Category":
return p.Category
case "Price":
return p.Price
}
return ""
}
func (d *productDS) Compare(item product, col string, other product) int {
return tt.StringCompare(d, item, col, other)
}
func (d *productDS) Matches(item product, filter string) bool {
return strings.Contains(item.Name, filter) || strings.Contains(item.Category, filter)
}
func sampleProducts() []product {
return []product{
{"1", "Widget A", "Widgets", "$9.99"},
{"2", "Gadget Pro", "Gadgets", "$49.99"},
{"3", "Widget B", "Widgets", "$12.99"},
{"4", "Gadget Lite", "Gadgets", "$19.99"},
}
}
// loadTable runs the initial data load synchronously (for use in examples).
func loadTable[T any](tbl *tt.Model[T]) {
cmd := tbl.Refresh()
if cmd != nil {
tbl.Update(cmd())
}
}
func main() {
src := &productDS{}
src.SetData(sampleProducts())
tbl := tt.New(src, context.Background(), []tt.Column{
tt.NewColumn("Name", "Name", tt.WithFlexRatio(1)),
})
tbl.SetWidth(60)
tbl.SetHeight(10)
loadTable(tbl)
fmt.Println("rows after load:", tbl.RowsCount())
// Add a new item and refresh — cursor stays in place.
src.SetData(append(src.Data(), product{"5", "New Item", "Misc", "$1.00"}))
loadTable(tbl)
fmt.Println("rows after refresh:", tbl.RowsCount())
}
Output: rows after load: 4 rows after refresh: 5
func (*Model[T]) Reload ¶
Reload reloads data from the datasource with a full reset: cursor, selection, and hScrollOffset are cleared. The filter is also cleared unless the datasource implements ServerFilterable, in which case it is preserved to stay in sync with the datasource's server-side filter state. This is equivalent to the former Refresh behaviour.
In paged mode Reload resets to page 0. In stream mode it resets to the beginning of the stream. In both cases cursor and scroll are always reset.
func (*Model[T]) RightFrozenColumns ¶
RightFrozenColumns returns the number of right frozen columns.
func (*Model[T]) RowIdIndex ¶
RowIdIndex returns the row index by RowId in the full set.
func (*Model[T]) ScrollIndicator ¶
func (m *Model[T]) ScrollIndicator() ScrollIndicatorConfig
ScrollIndicator returns the current scroll indicator configuration.
func (*Model[T]) ScrollLeft ¶
func (m *Model[T]) ScrollLeft()
ScrollLeft scrolls the table horizontally left.
func (*Model[T]) ScrollRight ¶
func (m *Model[T]) ScrollRight()
ScrollRight scrolls the table horizontally right.
func (*Model[T]) SelectAllRows ¶
SelectAllRows selects all filtered rows.
func (*Model[T]) SelectedRow ¶
SelectedRow returns the selected row at index in filteredRows.
func (*Model[T]) SelectedRows ¶
func (m *Model[T]) SelectedRows() []T
SelectedRows returns selected rows in the order they appear in filteredRows.
func (*Model[T]) SelectedRowsCount ¶
SelectedRowsCount returns the number of selected rows visible in filteredRows. Consistent with SelectedRows() and SelectionChangedMsg.Count.
func (*Model[T]) SelectionEnabled ¶
SelectionEnabled returns whether selection is enabled.
func (*Model[T]) SetAlternateRowsEnabled ¶
SetAlternateRowsEnabled enables or disables alternating rows.
func (*Model[T]) SetBottomBorderRenderer ¶
func (m *Model[T]) SetBottomBorderRenderer(fn BottomBorderRenderer[T], align Align, offset int)
SetBottomBorderRenderer sets the renderer for content embedded in the bottom border line, along with its horizontal alignment and offset from the edge (used by AlignLeft/AlignRight). Pass nil as fn to remove an existing renderer.
func (*Model[T]) SetCellStyler ¶
SetCellStyler sets an optional callback to override the resolved cell style on each render. The callback receives the row, column name, the base cell style (column style inherited from row style), and the current RowState. Pass nil to remove the styler.
func (*Model[T]) SetColumns ¶
SetColumns sets the table columns.
func (*Model[T]) SetDividers ¶
func (m *Model[T]) SetDividers(d TableDividers)
SetDividers sets the table dividers.
func (*Model[T]) SetFilter ¶
SetFilter sets the filter and recalculates filteredRows. If ds implements ServerFilterable[T], calls ds.Filter(filter) and triggers a load. If ds implements LocalFilterable[T], applies client-side filtering. Returns a tea.Batch with CursorMovedMsg and FilteredRowsChangedMsg. Also emits SelectionChangedMsg if a non-empty filter clears an active selection.
func (*Model[T]) SetFrozenColumns ¶
SetFrozenColumns sets the number of frozen columns.
func (*Model[T]) SetHeight ¶
SetHeight sets the table outer height including the border. Also recalculates pageSize for paged datasources.
func (*Model[T]) SetOuterBorder ¶
func (m *Model[T]) SetOuterBorder(b OuterBorder)
SetOuterBorder sets the outer border.
func (*Model[T]) SetRightFrozenColumns ¶
SetRightFrozenColumns sets the number of frozen columns on the right.
func (*Model[T]) SetRowStyler ¶
SetRowStyler sets an optional callback to override the resolved row style on each render. The callback receives the row, the base style computed from RowStyles, and the current RowState. Pass nil to remove the styler.
Example ¶
ExampleModel_SetRowStyler demonstrates applying a custom row style based on row content.
src := &productDS{}
src.SetData(sampleProducts())
tbl := tt.New(src, context.Background(), []tt.Column{
tt.NewColumn("Name", "Name", tt.WithFlexRatio(2)),
tt.NewColumn("Category", "Category", tt.WithFlexRatio(1)),
})
// Highlight "Gadgets" rows with a distinct foreground color.
tbl.SetRowStyler(func(p product, base lipgloss.Style, _ tt.RowState) lipgloss.Style {
if p.Category == "Gadgets" {
return base.Foreground(lipgloss.Color("214"))
}
return base
})
fmt.Println("row styler configured")
Output: row styler configured
func (*Model[T]) SetRowStyles ¶
SetRowStyles sets row styles.
func (*Model[T]) SetScrollIndicator ¶
func (m *Model[T]) SetScrollIndicator(c ScrollIndicatorConfig)
SetScrollIndicator sets the scroll indicator configuration.
func (*Model[T]) SetSelectionEnabled ¶
SetSelectionEnabled enables or disables row selection.
func (*Model[T]) SetShowHeader ¶
SetShowHeader sets header row visibility.
func (*Model[T]) SetSort ¶
SetSort sets sorting by the column named columnName. If ds implements ServerSortable[T], delegates to ds.Sort and triggers a load. If ds implements LocalSortable[T], applies client-side sorting. Returns ErrColumnNotSortable if the column is not marked with WithSortable().
func (*Model[T]) SetStripANSIForActiveStates ¶
SetStripANSIForActiveStates sets CursorStripANSI, SelectedStripANSI, and CursorSelectedStripANSI to strip on all active row states at once.
func (*Model[T]) ShowHeader ¶
ShowHeader returns whether the header is visible.
func (*Model[T]) SortActive ¶
SortActive reports whether sorting is currently applied. Use this to decide whether to show a sort indicator in your UI.
func (*Model[T]) SortColumn ¶
SortColumn returns the active sort column name.
func (*Model[T]) SortDirection ¶
func (m *Model[T]) SortDirection() SortDirection
SortDirection returns the current sort direction.
func (*Model[T]) ToggleRowSelection ¶
ToggleRowSelection toggles the selection of the row at index in filteredRows.
func (*Model[T]) ToggleSort ¶
ToggleSort cycles sort direction: Asc → Desc → clear.
Example ¶
ExampleModel_ToggleSort shows the Asc → Desc → clear cycle for a sortable column.
package main
import (
"context"
"fmt"
"strings"
tt "github.com/anadale/teaservice/teatable"
)
// product is the domain type used in teatable examples.
type product struct {
ID string
Name string
Category string
Price string
}
// productDS implements [tt.Datasource][product], [tt.LocalSortable][product],
// and [tt.LocalFilterable][product] using the embedded [tt.InMemorySource].
type productDS struct {
tt.InMemorySource[product]
}
func (d *productDS) RowId(p product) string { return p.ID }
func (d *productDS) CellValue(p product, col string) string {
switch col {
case "Name":
return p.Name
case "Category":
return p.Category
case "Price":
return p.Price
}
return ""
}
func (d *productDS) Compare(item product, col string, other product) int {
return tt.StringCompare(d, item, col, other)
}
func (d *productDS) Matches(item product, filter string) bool {
return strings.Contains(item.Name, filter) || strings.Contains(item.Category, filter)
}
func sampleProducts() []product {
return []product{
{"1", "Widget A", "Widgets", "$9.99"},
{"2", "Gadget Pro", "Gadgets", "$49.99"},
{"3", "Widget B", "Widgets", "$12.99"},
{"4", "Gadget Lite", "Gadgets", "$19.99"},
}
}
// loadTable runs the initial data load synchronously (for use in examples).
func loadTable[T any](tbl *tt.Model[T]) {
cmd := tbl.Refresh()
if cmd != nil {
tbl.Update(cmd())
}
}
func main() {
src := &productDS{}
src.SetData(sampleProducts())
tbl := tt.New(src, context.Background(), []tt.Column{
tt.NewColumn("Name", "Name", tt.WithFlexRatio(1), tt.WithSortable()),
})
tbl.SetWidth(60)
tbl.SetHeight(10)
loadTable(tbl)
_, _ = tbl.ToggleSort("Name") // first toggle → Asc
fmt.Println("asc:", tbl.SortActive(), tbl.SortDirection() == tt.SortAsc)
_, _ = tbl.ToggleSort("Name") // second toggle → Desc
fmt.Println("desc:", tbl.SortActive(), tbl.SortDirection() == tt.SortDesc)
_, _ = tbl.ToggleSort("Name") // third toggle → cleared
fmt.Println("cleared:", tbl.SortActive())
}
Output: asc: true true desc: true true cleared: false
func (*Model[T]) TotalCount ¶
TotalCount returns the total item count reported by the datasource (-1 if unknown).
func (*Model[T]) UnselectRow ¶
UnselectRow deselects the row at index in filteredRows.
func (*Model[T]) Update ¶
Update handles keyboard events and datasource messages.
Example ¶
ExampleModel_Update shows forwarding messages from a Bubble Tea Update function.
package main
import (
"context"
"fmt"
"strings"
tea "charm.land/bubbletea/v2"
tt "github.com/anadale/teaservice/teatable"
)
// product is the domain type used in teatable examples.
type product struct {
ID string
Name string
Category string
Price string
}
// productDS implements [tt.Datasource][product], [tt.LocalSortable][product],
// and [tt.LocalFilterable][product] using the embedded [tt.InMemorySource].
type productDS struct {
tt.InMemorySource[product]
}
func (d *productDS) RowId(p product) string { return p.ID }
func (d *productDS) CellValue(p product, col string) string {
switch col {
case "Name":
return p.Name
case "Category":
return p.Category
case "Price":
return p.Price
}
return ""
}
func (d *productDS) Compare(item product, col string, other product) int {
return tt.StringCompare(d, item, col, other)
}
func (d *productDS) Matches(item product, filter string) bool {
return strings.Contains(item.Name, filter) || strings.Contains(item.Category, filter)
}
func sampleProducts() []product {
return []product{
{"1", "Widget A", "Widgets", "$9.99"},
{"2", "Gadget Pro", "Gadgets", "$49.99"},
{"3", "Widget B", "Widgets", "$12.99"},
{"4", "Gadget Lite", "Gadgets", "$19.99"},
}
}
func main() {
src := &productDS{}
src.SetData(sampleProducts())
tbl := tt.New(src, context.Background(), []tt.Column{
tt.NewColumn("Name", "Name", tt.WithFlexRatio(1)),
})
tbl.SetWidth(60)
tbl.SetHeight(10)
// Forward any unhandled message to the table.
cmd := tbl.Update(tea.KeyPressMsg{Code: tea.KeyDown})
_ = cmd
fmt.Println("update handled")
}
Output: update handled
type MoreLoadedMsg ¶
MoreLoadedMsg is emitted after a Streamer.Load call (stream mode). Append=true means items should be appended; false means replace (initial load).
type Options ¶
type Options struct {
KeyMap KeyMap
RowStyles RowStyles
AlternateRowsEnabled bool
ShowHeader bool
SelectionEnabled bool
FrozenColumns int
RightFrozenColumns int
Title string
OuterBorder OuterBorder
Dividers TableDividers
ScrollIndicator ScrollIndicatorConfig
}
Options holds model creation parameters via functional options.
type OuterBorder ¶
type OuterBorder struct {
// Enabled turns on drawing the outer border.
Enabled bool
// Border defines the border characters.
Border lipgloss.Border
// Style sets the border style (color, attributes).
Style lipgloss.Style
}
OuterBorder defines the table outer border.
func DoubleBorder ¶
func DoubleBorder() OuterBorder
DoubleBorder returns an OuterBorder with double lines.
func NormalBorder ¶
func NormalBorder() OuterBorder
NormalBorder returns an OuterBorder with standard square corners.
func RoundedBorder ¶
func RoundedBorder() OuterBorder
RoundedBorder returns an OuterBorder with rounded corners.
func ThickBorder ¶
func ThickBorder() OuterBorder
ThickBorder returns an OuterBorder with thick lines.
type PageLoadedMsg ¶
PageLoadedMsg is emitted after a successful Pager.Load call (paged mode).
type Pager ¶
type Pager[T any] interface { Load(ctx context.Context, page int, size int) (items []T, totalCount int, err error) }
Pager loads a single page of items. totalCount is -1 when the total is unknown.
type RowStyles ¶
type RowStyles struct {
// Normal is the base style for a regular row.
Normal lipgloss.Style
// Alternate is the style for rows with odd index (0-based: 1, 3, 5...).
// Used only when AlternateRowsEnabled == true.
Alternate lipgloss.Style
// Cursor is the style for the row under the cursor.
// Overrides Normal and Alternate.
Cursor lipgloss.Style
// Selected is the style for a selected row.
// Overrides Normal and Alternate, but is overridden by Cursor.
Selected lipgloss.Style
// CursorSelected is the style for a row that is both under the cursor and selected.
// Overrides Cursor and Selected.
CursorSelected lipgloss.Style
// CursorStripANSI removes ANSI sequences from cell values when rendering rows in
// the cursor state, preventing embedded color codes from overriding the row style.
CursorStripANSI bool
// SelectedStripANSI removes ANSI sequences from cell values when rendering rows in
// the selected state, preventing embedded color codes from overriding the row style.
SelectedStripANSI bool
// CursorSelectedStripANSI removes ANSI sequences from cell values when rendering rows
// in the cursor+selected state, preventing embedded color codes from overriding the row style.
CursorSelectedStripANSI bool
}
RowStyles defines styles for different table row states.
func BlackAndWhiteRowStyles ¶
func BlackAndWhiteRowStyles() RowStyles
BlackAndWhiteRowStyles returns high-contrast row styles using only black and white: normal and alternate rows are white-on-black, while the cursor and cursor-selected states are inverted to black-on-white.
func DefaultRowStyles ¶
func DefaultRowStyles() RowStyles
DefaultRowStyles returns default row styles: - Cursor: inverted colors; - Selected: distinct from Normal (bold); - Alternate: empty style (visually the same as Normal until configured); - Normal: empty style.
type ScrollIndicatorConfig ¶
type ScrollIndicatorConfig struct {
// Mode defines how the indicator is displayed.
Mode ScrollIndicatorMode
// ThumbChar is the character for the thumb showing current position in the data.
ThumbChar string
// TrackChar is the character for the track the thumb moves along.
TrackChar string
// Style is the indicator rendering style.
Style lipgloss.Style
}
ScrollIndicatorConfig holds the vertical scroll indicator configuration.
func DataOnlyScrollIndicator ¶
func DataOnlyScrollIndicator() ScrollIndicatorConfig
DataOnlyScrollIndicator returns scroll indicator config spanning only the data area (ScrollIndicatorDataOnly). Uses "█" for thumb and "░" for track.
func DefaultScrollIndicator ¶
func DefaultScrollIndicator() ScrollIndicatorConfig
DefaultScrollIndicator returns the default scroll indicator config: indicator hidden (ScrollIndicatorHidden).
func FullHeightScrollIndicator ¶
func FullHeightScrollIndicator() ScrollIndicatorConfig
FullHeightScrollIndicator returns scroll indicator config spanning full table height (ScrollIndicatorFullHeight). Uses "█" for thumb and "░" for track.
type ScrollIndicatorMode ¶
type ScrollIndicatorMode int
ScrollIndicatorMode defines how the vertical scroll indicator is displayed.
const ( // ScrollIndicatorHidden — indicator is hidden (default). ScrollIndicatorHidden ScrollIndicatorMode = iota // ScrollIndicatorFullHeight — indicator spans full table height // (including header row and dividers). ScrollIndicatorFullHeight // ScrollIndicatorDataOnly — indicator spans only the data area // (without a header row and dividers). ScrollIndicatorDataOnly )
type SelectionChangedMsg ¶
type SelectionChangedMsg struct {
Count int // number of selected rows
Ids []string // RowIds in filteredRows order
}
SelectionChangedMsg emitted when set of selected rows changes.
type ServerFilterable ¶
ServerFilterable enables server-side filtering. Filter is a setter called before triggerLoad; it does not load data itself.
type ServerSortable ¶
type ServerSortable[T any] interface { Sort(column string, dir SortDirection) }
ServerSortable enables server-side sorting. Sort is a setter called before triggerLoad; it does not load data itself.
type SortDirection ¶
type SortDirection int
SortDirection defines sort order.
const ( // SortAsc — ascending order (A → Z). SortAsc SortDirection = iota // SortDesc — descending order (Z → A). SortDesc )
type Streamer ¶
type Streamer[T any] interface { Load(ctx context.Context, cursor string) (items []T, nextCursor string, hasMore bool, err error) }
Streamer loads the next batch of items using cursor-based pagination. Pass an empty cursor to start from the beginning.
type TableDividers ¶
type TableDividers struct {
// Header is the horizontal divider between header and data.
Header HorizontalDivider
// Rows is the horizontal divider between data rows.
Rows HorizontalDivider
// FrozenColumns is the vertical divider after the last frozen column.
FrozenColumns VerticalDivider
// Frozen is the vertical divider between frozen columns.
Frozen VerticalDivider
// Columns is the vertical divider between regular (non-frozen) columns.
Columns VerticalDivider
// RightFrozenColumns is the vertical divider before the first right frozen column.
RightFrozenColumns VerticalDivider
// RightFrozen is the vertical divider between right frozen columns.
RightFrozen VerticalDivider
}
TableDividers groups all table dividers.
func ASCIIDividers ¶
func ASCIIDividers() TableDividers
ASCIIDividers returns a set with all five dividers (ASCII characters).
func FullGridDividers ¶
func FullGridDividers() TableDividers
FullGridDividers returns a set with all five dividers (Unicode).
func SimpleDividers ¶
func SimpleDividers() TableDividers
SimpleDividers returns a set with only the header divider enabled.
func StyledFullGridDividers ¶
func StyledFullGridDividers(style lipgloss.Style) TableDividers
StyledFullGridDividers returns a set with all five dividers (Unicode).
type VerticalDivider ¶
type VerticalDivider struct {
// Enabled turns on drawing the divider.
Enabled bool
// Char is the main character of the vertical line (e.g. │).
Char string
// TopJoin is the character joining the top border (e.g. ┬).
TopJoin string
// BottomJoin is the character joining the bottom border (e.g. ┴).
BottomJoin string
// HCrossing is used when a horizontal divider fully crosses this vertical divider (e.g. ╫ for ║+─).
// Falls back to the horizontal divider's Crossing if empty.
HCrossing string
// HCrossingRight is used when only the right side has horizontal content at this crossing (e.g. ╟).
// Falls back to HCrossing, then the horizontal divider's LeftJoin.
HCrossingRight string
// HCrossingLeft is used when only the left side has horizontal content at this crossing (e.g. ╢).
// Falls back to HCrossing, then the horizontal divider's RightJoin.
HCrossingLeft string
// HCrossingDown is used when only the downward direction exists at this crossing (e.g. ╥).
// Falls back to HCrossing, then the horizontal divider's CrossingDown.
HCrossingDown string
// Style sets the divider style.
Style lipgloss.Style
}
VerticalDivider defines a vertical divider (between columns).
func SimpleColumnsDivider ¶
func SimpleColumnsDivider() VerticalDivider
SimpleColumnsDivider returns a vertical divider between regular columns.
func SimpleFrozenColumnsDivider ¶
func SimpleFrozenColumnsDivider() VerticalDivider
SimpleFrozenColumnsDivider returns a vertical divider after the last frozen column.
func SimpleFrozenDivider ¶
func SimpleFrozenDivider() VerticalDivider
SimpleFrozenDivider returns a vertical divider between frozen columns.
func SimpleRightFrozenColumnsDivider ¶
func SimpleRightFrozenColumnsDivider() VerticalDivider
SimpleRightFrozenColumnsDivider returns a vertical divider before the first right frozen column.
func SimpleStyledColumnsDivider ¶
func SimpleStyledColumnsDivider(style lipgloss.Style) VerticalDivider
SimpleStyledColumnsDivider returns a vertical divider between regular columns.
func SimpleStyledFrozenColumnsDivider ¶
func SimpleStyledFrozenColumnsDivider(style lipgloss.Style) VerticalDivider
SimpleStyledFrozenColumnsDivider returns a vertical divider after the last frozen column.
func SimpleStyledFrozenDivider ¶
func SimpleStyledFrozenDivider(style lipgloss.Style) VerticalDivider
SimpleStyledFrozenDivider returns a vertical divider between frozen columns.
func SimpleStyledRightFrozenColumnsDivider ¶
func SimpleStyledRightFrozenColumnsDivider(style lipgloss.Style) VerticalDivider
SimpleStyledRightFrozenColumnsDivider returns a vertical divider before the first right frozen column.