Documentation
¶
Overview ¶
Package grid is a product-neutral result-grid transcript block: a bubble-table-backed table with sort, per-cell column selection, a scrollbar, style presets, and slots for a product's own secondary views (charts, a current-row card, raw responses, ...) and split-pane layout. Ported and generalised from DataTug chat's GridModel/gridState (datatug-cli/pkg/chat/grid.go, ui.go, recordset_ui.go, recordset_views.go, table_style.go). DataTug and Sneat Chat use this one grid for tabular/contact data — not two competing ones; DataTug's gridState is a thin wrapper embedding a *grid.Model.
Keybindings ¶
↑↓/k/j move the row (a product's own WithKeyHandler is checked first and can still claim "j"/"down" for its own use, e.g. DataTug's join-candidate navigation, by handling it before the grid's default runs); ←→/h/l select a column, auto-scrolling it into view (SelectedColumn); digit keys switch views ("1" is always the table, "2".. select a registered ExtraView in order); Tab toggles focus between the table and a split secondary view (ToggleSecondaryFocusIfSplit); s sorts (toggling ascending/descending) by the selected column; Enter emits RowActivatedMsg; + emits tui.AddToSidebarMsg for the highlighted row's Ref; / opens bubble-table's built-in filter. While the filter input is focused, CapturesEsc reports true so a surrounding chatshell lets Esc clear/blur the filter before doing anything else.
A product's WithKeyHandler hook is checked first, for every key the filter isn't consuming, and can claim any of the above (e.g. DataTug's Enter opens a cell-detail dialog instead of emitting RowActivatedMsg, and c/r/a/d/b/B/e/q/space are entirely DataTug's own workspace actions with no generic-grid meaning at all).
Rows and values ¶
Row.Values is positional (aligned with the Columns slice the Row was built against), not a map keyed by column name, so two columns sharing a name (e.g. `SELECT a.id, b.id`) each keep their own value. Use grid.Absent for a column with no value at all for a row (a sparse selection), which renders differently from an explicit nil ("NULL"). A Row.Values entry may be a raw Go value (formatted by FormatValue) or a product's own pre-formatted display string (e.g. DataTug's date-only formatting) — Sort and the table cells use whichever was supplied. Row.Key, when set to a stable identifier (e.g. the row's original/source index), survives Sort; IndexForKey finds it again after a sort or a data refresh.
Views ¶
There is no fixed Card/Inspector view: the built-in table is always view 0; everything else is a product-registered ExtraView (WithExtraViews / SetExtraViews), in whatever order the product wants. CardView and InspectorView are ready-made ExtraView constructors — a formatted vertical field list and a raw-Go-value dump of the highlighted row, respectively — for a product that wants one (DataTug registers CardView as its "Current row" view, third in its own Table/Charts/Current row/Raw/Headers order).
A product registers its own secondary views (DataTug's Charts, Raw response, Headers) with WithExtraViews, and its split-pane policy — table and the active secondary view side by side when there's room, generalising DataTug's chooseRecordsetLayout — with WithSplitLayout (Model.NaturalWidth gives the LayoutFunc the table's unclipped content width to compare against the pane's total width). Large results are capped to DefaultMaxVisibleRows (or WithMaxVisibleRows's value) per page so a 1000-row result never renders fully into a scrolling transcript.
Style ¶
WithStyle/SetStyle pick a Style preset (StyleLines, StyleSoft, StyleMinimal are built in); ParseStyle recovers one by name, e.g. from a persisted session. WithFooterHook lets a product append its own text (e.g. a save-status badge) to the grid's built-in stats footer (row/column range, sort indicator).
Adoption ¶
DataTug adopts it by mapping a secureread.Result to Columns/Rows:
cols := make([]grid.Column, len(result.Columns))
for i, name := range result.Columns {
cols[i] = grid.Column{Name: name, Numeric: columnIsNumeric(result.Rows, name)}
}
rows := make([]grid.Row, len(result.Rows))
for i, row := range result.Rows {
values := make([]any, len(result.Columns))
for c, name := range result.Columns {
if v, ok := row.Data[name]; ok {
values[c] = v
} else {
values[c] = grid.Absent
}
}
rows[i] = grid.Row{Key: strconv.Itoa(i), Values: values}
}
m := grid.New(cols, rows, grid.WithTitle(title),
grid.WithExtraViews(chartsView, grid.CardView("Current row"), rawView, headersView),
grid.WithSplitLayout(chooseRecordsetLayout),
grid.WithKeyHandler(dataTugGridActions))
Index ¶
- Constants
- Variables
- func FormatValue(value any) string
- type Column
- type ExtraView
- type FooterHook
- type KeyHandler
- type LayoutFunc
- type Model
- func (m *Model) ActiveViewContent(width, height int) string
- func (m *Model) CapturesEsc() bool
- func (m *Model) Cell(rowIndex, columnIndex int) string
- func (m *Model) ColumnOffset() int
- func (m *Model) Columns() []Column
- func (m *Model) Current() *session.EntityRef
- func (m *Model) CurrentIndex() int
- func (m *Model) CurrentRow() (Row, bool)
- func (m *Model) CurrentView() View
- func (m *Model) ExtraViews() []ExtraView
- func (m *Model) Focusable() bool
- func (m *Model) Focused() bool
- func (m *Model) Footer() string
- func (m *Model) HeaderLine(width int) string
- func (m *Model) IndexForKey(key string) int
- func (m *Model) NaturalWidth() int
- func (m *Model) Rows() []Row
- func (m *Model) SecondaryFocus() bool
- func (m *Model) SelectColumn(index int)
- func (m *Model) SelectRow(index int)
- func (m *Model) SelectedColumn() int
- func (m *Model) SetExtraViews(views ...ExtraView)
- func (m *Model) SetFocused(focused bool)
- func (m *Model) SetFooterHook(fn FooterHook)
- func (m *Model) SetKeyHandler(fn KeyHandler)
- func (m *Model) SetSecondaryFocus(focused bool)
- func (m *Model) SetStyle(s Style)
- func (m *Model) SetTitle(title string)
- func (m *Model) SetView(v View)
- func (m *Model) SetWidth(width int)
- func (m *Model) Sort(column int)
- func (m *Model) SortState() (column int, desc bool)
- func (m *Model) Style() Style
- func (m *Model) TableView() string
- func (m *Model) Title() string
- func (m *Model) ToggleSecondaryFocusIfSplit() bool
- func (m *Model) Update(msg tea.Msg) (transcript.Block, tea.Cmd)
- func (m *Model) View(width int, focused bool) string
- func (m *Model) VisibleColumnRange() (int, int)
- func (m *Model) VisibleIndices() (int, int)
- func (m *Model) Width() int
- type Option
- func WithExtraViews(views ...ExtraView) Option
- func WithFilterDisabled() Option
- func WithFooterHook(fn FooterHook) Option
- func WithInitialSort(column int, desc bool) Option
- func WithKeyHandler(fn KeyHandler) Option
- func WithMaxVisibleRows(n int) Option
- func WithSplitLayout(fn LayoutFunc) Option
- func WithStyle(s Style) Option
- func WithTitle(title string) Option
- func WithoutViewSwitcher() Option
- type Row
- type RowActivatedMsg
- type SplitLayout
- type Style
- type View
Constants ¶
const DefaultMaxVisibleRows = 12
DefaultMaxVisibleRows is the page size a Model uses when WithMaxVisibleRows is not supplied.
Variables ¶
var ( StyleLines = Style{ Name: "Lines", BorderColor: lipgloss.Color("241"), HeaderStyle: lipgloss.NewStyle().Background(lipgloss.Color("237")).Foreground(lipgloss.Color("255")).Bold(true), } StyleSoft = Style{ Name: "Soft", BorderColor: lipgloss.Color("235"), HeaderStyle: lipgloss.NewStyle().Background(lipgloss.Color("236")).Foreground(lipgloss.Color("250")).Bold(true), } StyleMinimal = Style{ Name: "Minimal", BorderColor: lipgloss.Color("232"), HeaderStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("250")).Bold(true), } )
Built-in style presets, ported from DataTug's table_style.go.
var Absent any = absentType{}
Absent is the sentinel Row.Values entry meaning "no value was supplied for this column" — distinct from an explicit nil ("NULL"). Product adapters use it for sparse selections (e.g. DataTug's cell-range picks) where only some columns have a value for a given row.
var Styles = []Style{StyleLines, StyleSoft, StyleMinimal}
Styles lists the built-in presets in cycling order.
Functions ¶
func FormatValue ¶
FormatValue applies basic terminal-safe value formatting, shared with the card/inspector views.
Types ¶
type ExtraView ¶
type ExtraView struct {
// Label is shown in the view switcher header, e.g. "Charts".
Label string
// ShortLabel is shown instead of Label once the header is too narrow
// for the full text (see viewLabels) — main's own fixed forms
// ("Charts" → "C", "Current row" → "Row") rather than a generic
// N-character truncation of Label, which can make two labels
// indistinguishable once both are cut to the same length (e.g.
// "Charts"/"Current row" both truncating to "Ch"/"Cu" reads fine, but
// a runt truncation of arbitrary text has no such guarantee). Falls
// back to Label's own generic truncation when empty.
ShortLabel string
// Render draws the view's body at the given content width/height.
Render func(m *Model, width, height int) string
// Update optionally handles a key press while this view is active and
// focused (e.g. arrow keys moving between chart candidates). It returns
// the command to run (if any) and whether it handled the message; when
// it returns false the grid's own key handling still runs.
Update func(m *Model, msg tea.KeyPressMsg) (tea.Cmd, bool)
}
ExtraView is a product-registered secondary view — DataTug's Charts, Raw response, Headers and current-row views are ExtraViews — shown alongside the table and selected the same way (number keys, cycling through the header). A grid stays the one generic component; products supply their own panes (or the CardView/InspectorView helpers) instead of building a competing grid.
func CardView ¶
CardView returns an ExtraView rendering the highlighted row as a formatted vertical field list (Column name / FormatValue'd value). label defaults to "Current row" when empty; its ShortLabel is main's own "Row". Ported from DataTug's recordset_views.go currentRowContent (raw=false).
func InspectorView ¶
InspectorView is CardView's raw-value counterpart: it renders each field's Go value (%#v) instead of FormatValue's terminal-safe text. label defaults to "Inspector" when empty, ShortLabel to "Insp".
type FooterHook ¶
FooterHook lets a product append extra stats to the grid's own footer (row/column range, sort indicator), e.g. a version badge or a save-status note. It receives the built-in footer text and returns the final text.
type KeyHandler ¶
KeyHandler lets a product own specific key presses (e.g. DataTug's c/r/a/d/b/s/B/e actions) instead of the grid's own defaults. It is checked first, for every key press the filter input isn't consuming; returning handled=false falls through to the grid's built-in handling (column/row navigation, view switching, sort, Enter, +, /).
type LayoutFunc ¶
type LayoutFunc func(totalWidth, naturalWidth int, view View) SplitLayout
LayoutFunc chooses, for the active non-table view, whether to split the pane between the table and that view. totalWidth is the grid's full width; naturalWidth is the table's natural (unclipped) content width, from Model.NaturalWidth(). Generalises DataTug's chooseRecordsetLayout so a product's split-pane policy is a plugged-in function, not a second grid.
type Model ¶
type Model struct {
// contains filtered or unexported fields
}
Model is a transcript.EntityBlock: a result grid with a sortable/filterable table, per-cell column selection, a scrollbar, style presets, and slots for a product's own secondary views (ExtraView) and split-pane layout. Ported and generalised from DataTug's GridModel/gridState, recordset_ui.go, recordset_views.go and table_style.go.
func (*Model) ActiveViewContent ¶
ActiveViewContent renders just the active view's own body (table or the active ExtraView) at the given size, without any card chrome and without the OTHER pane a split layout would show alongside it. A product's test wants this instead of the full View() output whenever a split layout could put the table's own header/cells within reach of a substring match aimed only at the secondary view (e.g. asserting a CardView's scroll position by checking which fields are currently rendered).
func (*Model) CapturesEsc ¶
CapturesEsc reports whether the grid's own filter input is currently focused, in which case Esc should clear/blur that filter rather than be handled by a surrounding chatshell (e.g. to close the block or the pane).
func (*Model) Cell ¶
Cell returns the formatted display text for a row/column (the same text shown in the table), or "" out of bounds.
func (*Model) ColumnOffset ¶
ColumnOffset is the index of the first horizontally-scrolled-into-view column in the table.
func (*Model) CurrentIndex ¶
CurrentIndex returns the display index of the highlighted row, or -1 when there are no rows. It is filter-aware: bubble-table's cursor indexes GetVisibleRows() (the post-filter subset), so the row's hidden sourceKey metadata — not the raw cursor index — is what recovers the position in Model.rows.
func (*Model) CurrentRow ¶
CurrentRow returns the highlighted Row and whether one exists.
func (*Model) ExtraViews ¶
ExtraViews returns the currently registered extra views.
func (*Model) Footer ¶
Footer returns the grid's current footer text (row/column range, sort indicator, plus any WithFooterHook/SetFooterHook text) — the same text shown in the card's bottom border.
func (*Model) HeaderLine ¶
HeaderLine returns the card's title bar content at a given width — the same text embedded in the top border by View — without rendering the whole card.
func (*Model) IndexForKey ¶
IndexForKey returns the display index of the row whose Key equals key, or -1. Row.Key is preserved across Sort, so a product can save a row's Key (e.g. a stable source-record index) and restore the selection after a sort or a data refresh.
func (*Model) NaturalWidth ¶
NaturalWidth is the table's unclipped content width (sum of column widths plus borders), for a LayoutFunc to compare against the pane's total width — the same quantity DataTug's chooseRecordsetLayout compares against.
func (*Model) SecondaryFocus ¶
SecondaryFocus reports whether keyboard focus is on the active secondary (non-table) view rather than the table, when the pane is split. See SetSecondaryFocus.
func (*Model) SelectColumn ¶
SelectColumn selects a column directly (as h/l do interactively), clamping to bounds and scrolling it into view.
func (*Model) SelectRow ¶
SelectRow highlights the row at the given display index (a position in Model.rows/Rows(), the same index space as IndexForKey — NOT bubble- table's own cursor position, which indexes the filtered/visible subset). It does not change SelectedColumn. A no-op when that row is currently filtered out of view.
func (*Model) SelectedColumn ¶
SelectedColumn is the column h/l (or SelectColumn) currently has selected.
func (*Model) SetExtraViews ¶
SetExtraViews replaces the registered extra views (e.g. once an HTTP response becomes available and a product wants to add Raw/Headers views that weren't known at construction time). The active view is reset to ViewTable if it no longer resolves.
func (*Model) SetFocused ¶
SetFocused sets the grid's focus state directly, for a caller that renders its own width/focus rather than going through the transcript.Block View signature (e.g. a modal dialog's own grid).
func (*Model) SetFooterHook ¶
func (m *Model) SetFooterHook(fn FooterHook)
SetFooterHook registers (or replaces) the product footer hook after construction. See WithFooterHook.
func (*Model) SetKeyHandler ¶
func (m *Model) SetKeyHandler(fn KeyHandler)
SetKeyHandler registers (or replaces) the product key-handler hook after construction — useful when the hook's closure needs context only available once the Model itself exists (e.g. a dialog capturing its own *Model to react to Space/Enter).
func (*Model) SetSecondaryFocus ¶
SetSecondaryFocus moves keyboard focus to/from the active secondary view. It is a no-op (always false) while the table view is active. Ported from DataTug's gridState.setSecondaryFocus.
func (*Model) SetTitle ¶
SetTitle changes the grid's header title after construction (e.g. a version badge DataTug prefixes onto it once an HTTP refresh is compared against its parent).
func (*Model) SetView ¶
SetView switches the active view (ViewTable or a registered ExtraView index), clamped to a valid value. Mirrors DataTug's gridState.setRecordsetView, auto-focusing the new secondary view when it won't be split with the table.
func (*Model) Sort ¶
Sort toggles ascending/descending order on column, stably. Ported from DataTug's GridModel.Sort (pkg/chat/grid.go).
func (*Model) TableView ¶
TableView renders just the inner table (no card border/scrollbar/footer), at the Model's last-set width, for a caller that wants to embed it in its own chrome rather than grid.Model's own View.
func (*Model) ToggleSecondaryFocusIfSplit ¶
ToggleSecondaryFocusIfSplit toggles SecondaryFocus when the active non-table view is currently sharing the pane with the table (per the registered LayoutFunc), and reports whether it did. A product's own Tab handling (e.g. falling back to focusing its composer) uses the return value to know whether the grid consumed the key.
func (*Model) Update ¶
Update implements transcript.Block. Key handling order: the filter input (while focused) always wins; then, ONLY while secondary focus holds the active non-table view (see SecondaryFocus/SetSecondaryFocus — always true for a non-split view, since the table isn't reachable there), that ExtraView's own Update; then the product KeyHandler (see WithKeyHandler), which can claim any key including Enter, digits or Tab; then the grid's own defaults: h/l select a column (scrolling it into view), up/k and down move the table row WHILE THE TABLE HAS FOCUS (a split layout's primary pane, or the plain table view), digit keys switch views (1 is always the table), Tab toggles focus between the table and a split secondary view, s sorts by the selected column, Enter emits RowActivatedMsg, + emits tui.AddToSidebarMsg, / opens the built-in filter.
func (*Model) View ¶
View implements transcript.Block: a bordered card (title + view switcher, content, scrollbar down the right edge, a stats footer in the bottom border) around the active view's body — the table by default, or a registered ExtraView, optionally split side by side with the table per WithSplitLayout. Ported from DataTug's gridState.viewWithTitle/recordsetView/recordsetHeader.
func (*Model) VisibleColumnRange ¶
VisibleColumnRange returns the 1-based (first, last) column numbers currently rendered in the table (0, 0 when the pane is too narrow to show any full data column, only bubble-table's overflow marker).
func (*Model) VisibleIndices ¶
VisibleIndices returns the display-index range (inclusive) of the table's current page, or (0, -1) with no rows.
type Option ¶
type Option func(*Model)
Option configures a Model at construction time.
func WithExtraViews ¶
WithExtraViews registers product-specific secondary views (e.g. DataTug's Charts/Current-row/Raw/Headers) after the built-in table view, in the given order. They are selected the same way: number keys and the header switcher. See also Model.SetExtraViews for registering them after construction (e.g. once an HTTP response becomes available).
func WithFilterDisabled ¶ added in v0.0.2
func WithFilterDisabled() Option
WithFilterDisabled turns off bubble-table's built-in "/" row filter entirely — no filter typing, no CapturesEsc-while-filtering state — for a grid where that isn't a meaningful operation (e.g. DataTug's bookmark, dock and parameter-lookup grids, which already show a narrow, purpose- built row set) or where the product wants "/" for something else.
func WithFooterHook ¶
func WithFooterHook(fn FooterHook) Option
WithFooterHook registers the product footer hook (see FooterHook).
func WithInitialSort ¶
WithInitialSort records that rows are already ordered by column/desc (e.g. a product re-fetched pre-sorted data, such as DataTug's view-backed docks, rather than calling Sort itself), without re-sorting them. It only sets the grid's own sort-state bookkeeping — the footer's sort indicator and, importantly, the toggle direction the NEXT Sort(column) call picks — to match rows the caller has already arranged. Ported from DataTug's rebuildDockGrids initializing GridModel.sortColumn/sortDesc from the backing View's persisted OrderBy/Descending.
func WithKeyHandler ¶
func WithKeyHandler(fn KeyHandler) Option
WithKeyHandler registers the product key-handler hook (see KeyHandler).
func WithMaxVisibleRows ¶
WithMaxVisibleRows caps how many rows the table view renders per page so a large result (e.g. 1000 rows) never renders fully into a scrolling transcript. Defaults to DefaultMaxVisibleRows; pass 0 to disable paging.
func WithSplitLayout ¶
func WithSplitLayout(fn LayoutFunc) Option
WithSplitLayout registers the policy used to decide whether a non-table view shares the pane with the table (side by side) or takes the full width. Without it, a non-table view always takes the full pane, matching prior behaviour.
func WithStyle ¶
WithStyle sets the grid's initial border/header color preset (see Style). Defaults to StyleLines.
func WithoutViewSwitcher ¶ added in v0.0.2
func WithoutViewSwitcher() Option
WithoutViewSwitcher hides the "1 Table [· 2 Charts ...]" view-switcher text from the header entirely — main's own title-only header for a grid with no other views worth advertising (DataTug's bookmark, dock and parameter-lookup grids). Digit keys still switch views if any are registered; this only affects what the header displays.
type Row ¶
Row is one grid row. Values is positional, aligned with the Columns slice the Row was built against — not a map keyed by column name — so two columns sharing a name (e.g. `SELECT a.id, b.id`) each keep their own value. A product may pass either raw Go values (formatted by FormatValue) or its own pre-formatted display strings (e.g. DataTug's date-only formatting) — both are valid Row.Values entries. Ref, when set, lets the row be pinned to the sidebar or resolved as the "current" entity (transcript.EntityBlock). Key, when set to a stable identifier (e.g. the row's original/source index), survives Sort — IndexForKey resolves it back to a display index.
type RowActivatedMsg ¶
type RowActivatedMsg struct{ Row Row }
RowActivatedMsg is emitted on Enter over the highlighted row (table view) when no KeyHandler claims "enter" first.
type SplitLayout ¶
SplitLayout is the result of a LayoutFunc: whether the secondary (non-table) view should share the pane with the table, and at what widths.
type Style ¶
Style is a grid's border/header color preset. It is presentation only: changing it never rewrites rows or columns. Ported from DataTug's table_style.go (tableStyle), generalised so any product can define and cycle through its own presets.
func ParseStyle ¶
ParseStyle finds a built-in preset by Name (e.g. as persisted in a saved session), defaulting to StyleLines for an unknown or empty name.
type View ¶
type View int
View selects what the grid's secondary area shows: the built-in table (ViewTable, always index 0), or a product-registered ExtraView (index 1..len(extraViews), in registration order). Unlike an earlier revision, there is no fixed Card/Inspector view: use the CardView/InspectorView constructors below to register one (or both, in whatever order) as an ExtraView, alongside a product's own (Charts, Raw response, ...).
const ViewTable View = 0
ViewTable is the sortable/filterable table (the default, always index 0).