Showing preview only (297K chars total). Download the full file or copy to clipboard to get everything.
Repository: Evertras/bubble-table
Branch: main
Commit: ef37988ad03a
Files: 86
Total size: 267.0 KB
Directory structure:
gitextract_ij2y60qv/
├── .editorconfig
├── .github/
│ └── workflows/
│ ├── build.yml
│ ├── codeql-analysis.yml
│ ├── coverage.yml
│ └── lint.yml
├── .gitignore
├── .go-version
├── .golangci.yaml
├── CONTRIBUTING.md
├── LICENSE
├── Makefile
├── README.md
├── examples/
│ ├── dimensions/
│ │ ├── README.md
│ │ └── main.go
│ ├── events/
│ │ ├── README.md
│ │ └── main.go
│ ├── features/
│ │ ├── README.md
│ │ └── main.go
│ ├── filter/
│ │ ├── README.md
│ │ └── main.go
│ ├── filterapi/
│ │ ├── README.md
│ │ └── main.go
│ ├── flex/
│ │ ├── README.md
│ │ └── main.go
│ ├── metadata/
│ │ ├── README.md
│ │ └── main.go
│ ├── multiline/
│ │ ├── README.md
│ │ └── main.go
│ ├── pagination/
│ │ ├── README.md
│ │ └── main.go
│ ├── pokemon/
│ │ ├── README.md
│ │ └── main.go
│ ├── scrolling/
│ │ ├── README.md
│ │ └── main.go
│ ├── simplest/
│ │ ├── README.md
│ │ └── main.go
│ ├── sorting/
│ │ ├── README.md
│ │ └── main.go
│ └── updates/
│ ├── README.md
│ ├── data.go
│ └── main.go
├── flake.nix
├── go.mod
├── go.sum
└── table/
├── benchmarks_test.go
├── border.go
├── calc.go
├── calc_test.go
├── cell.go
├── column.go
├── column_test.go
├── data.go
├── data_test.go
├── dimensions.go
├── dimensions_test.go
├── doc.go
├── events.go
├── events_test.go
├── filter.go
├── filter_test.go
├── footer.go
├── header.go
├── keys.go
├── keys_test.go
├── model.go
├── model_test.go
├── options.go
├── options_test.go
├── overflow.go
├── pagination.go
├── pagination_test.go
├── query.go
├── query_test.go
├── row.go
├── scrolling.go
├── scrolling_fuzz_test.go
├── scrolling_test.go
├── sort.go
├── sort_test.go
├── strlimit.go
├── strlimit_test.go
├── update.go
├── update_test.go
├── view.go
├── view_selectable_test.go
└── view_test.go
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
root = true
[*]
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
end_of_line = lf
[{*.go,go.*}]
indent_size = 4
indent_style = tab
[Makefile]
indent_style = tab
================================================
FILE: .github/workflows/build.yml
================================================
name: build
on:
push:
tags:
- v*
branches:
- main
pull_request:
jobs:
buildandtest:
name: Build and test
strategy:
matrix:
go-version: [~1.18, ^1]
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Install Go
uses: actions/setup-go@v4
with:
go-version: ${{ matrix.go-version }}
- name: Checkout code
uses: actions/checkout@v4
- name: Download Go modules
run: go mod download
- name: Build
run: go build -v ./table
- name: Test
run: go test -race ./table
================================================
FILE: .github/workflows/codeql-analysis.yml
================================================
name: "CodeQL"
on:
push:
branches: [ main ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
schedule:
- cron: '29 4 * * 4'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'go' ]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# ℹ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
================================================
FILE: .github/workflows/coverage.yml
================================================
name: coverage
on:
push:
tags:
- v*
branches:
- main
pull_request:
jobs:
coverage:
name: Report Coverage
runs-on: ubuntu-latest
steps:
- name: Install Go
uses: actions/setup-go@v4
with:
go-version: "1.18.10"
- name: Check out code
uses: actions/checkout@v4
- name: Install deps
run: |
go mod download
- name: Run tests with coverage output
run: |
go test -race -covermode atomic -coverprofile=covprofile ./...
- name: Install goveralls
run: go install github.com/mattn/goveralls@latest
- name: Send coverage
env:
COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: goveralls -coverprofile=covprofile -service=github
================================================
FILE: .github/workflows/lint.yml
================================================
name: golangci-lint
on:
push:
tags:
- v*
branches:
- main
pull_request:
permissions:
contents: read
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
# For whatever reason, 1.21.9 blows up because it can't
# find 'max' in some go lib... pinning to 1.21.4 fixes this
- name: Install Go
uses: actions/setup-go@v4
with:
go-version: 1.21.4
- name: Checkout code
uses: actions/checkout@v4
- name: Lint
run: make lint
================================================
FILE: .gitignore
================================================
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
/bin
# Test binary, built with `go test -c`
*.test
# Profiling outputs
*.prof
# Outputs of the go coverage tool, specifically when used with LiteIDE
*.out
*.coverage
# Go vendor folder
vendor/
# Mac stuff
.DS_Store
# Envrc with direnv
.envrc
# Sandbox area for experimenting without worrying about git
sandbox
================================================
FILE: .go-version
================================================
1.17.6
================================================
FILE: .golangci.yaml
================================================
version: "2"
linters:
enable:
- asciicheck
- bidichk
- bodyclose
- contextcheck
- cyclop
- depguard
- dogsled
- dupl
- durationcheck
- err113
- errname
- errorlint
- exhaustive
- forbidigo
- forcetypeassert
- funlen
- gocognit
- goconst
- gocyclo
- godot
- goheader
- gomoddirectives
- gomodguard
- goprintffuncname
- gosec
- importas
- ireturn
- lll
- makezero
- misspell
- mnd
- nakedret
- nestif
- nilerr
- nilnil
- nlreturn
- noctx
- nolintlint
- prealloc
- predeclared
- promlinter
- revive
- rowserrcheck
- sqlclosecheck
- staticcheck
- tagliatelle
- thelper
- tparallel
- unconvert
- unparam
- varnamelen
- wastedassign
- whitespace
- wrapcheck
settings:
depguard:
rules:
main:
list-mode: lax
allow:
- github.com/stretchr/testify/assert
- github.com/charmbracelet/lipgloss
- github.com/muesli/reflow/wordwrap
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
formatters:
enable:
- gci
- gofmt
- goimports
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing
Thanks for your interest in contributing!
## Contributing issues
Please feel free to open an issue if you think something is working incorrectly,
if you have a feature request, or if you just have any questions. No templates
are in place, all I ask is that you provide relevant information if you believe
something is working incorrectly so we can sort it out quickly.
## Contributing code
All contributions should have an associated issue. If you are at all unsure
about how to solve the issue, please ask! I'd rather chat about possible
solutions than have someone spend hours on a PR that requires a lot of major
changes.
Test coverage is important. If you end up with a small (<1%) drop, I'm happy to
help cover the gap, but generally any new features or changes should have some
tests as well. If you're not sure how to test something, feel free to ask!
Linting can be done with `make lint`. Running `fmt` can be done with `make fmt`.
Tests can be run with `make test`.
Doing all these before submitting the PR can help you pass the existing
gatekeeping tests without having to wait for them to run every time.
The name of the PR, commit messages, and branch names aren't very important,
there are no special triggers or filters or anything in place that depend on names.
The name of the PR should at least reasonably describe the change, because this
is what people will see in the commit history and what goes into the change logs.
Exported functions should generally follow the pattern of returning a `Model`
and not use a pointer receiver. This matches more closely with the flow of Bubble
Tea (and Elm), and discourages users from making mutable changes with unintended
side effects. Unexported functions are free to use pointer receivers for both
simplicity and performance reasons.
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2022 Brandon Fulljames
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: Makefile
================================================
ifeq ($(OS), Windows_NT)
EXE_EXT=.exe
else
EXE_EXT=
endif
.PHONY: example-pokemon
example-pokemon:
@go run ./examples/pokemon/*.go
.PHONY: example-metadata
example-metadata:
@go run ./examples/metadata/*.go
.PHONY: example-dimensions
example-dimensions:
@go run ./examples/dimensions/main.go
.PHONY: example-events
example-events:
@go run ./examples/events/main.go
.PHONY: example-features
example-features:
@go run ./examples/features/main.go
.PHONY: example-multiline
example-multiline:
@go run ./examples/multiline/main.go
.PHONY: example-filter
example-filter:
@go run ./examples/filter/*.go
.PHONY: example-filterapi
example-filterapi:
@go run ./examples/filterapi/*.go
.PHONY: example-flex
example-flex:
@go run ./examples/flex/*.go
.PHONY: example-pagination
example-pagination:
@go run ./examples/pagination/*.go
.PHONY: example-simplest
example-simplest:
@go run ./examples/simplest/*.go
.PHONY: example-scrolling
example-scrolling:
@go run ./examples/scrolling/*.go
.PHONY: example-sorting
example-sorting:
@go run ./examples/sorting/*.go
.PHONY: example-updates
example-updates:
@go run ./examples/updates/*.go
.PHONY: test
test:
@go test -race -cover ./table
.PHONY: test-coverage
test-coverage: coverage.out
@go tool cover -html=coverage.out
.PHONY: benchmark
benchmark:
@go test -run=XXX -bench=. -benchmem ./table
.PHONY: lint
lint: ./bin/golangci-lint$(EXE_EXT)
@./bin/golangci-lint$(EXE_EXT) run ./table
coverage.out: table/*.go go.*
@go test -coverprofile=coverage.out ./table
.PHONY: fmt
fmt: ./bin/gci$(EXE_EXT)
@go fmt ./...
@./bin/gci$(EXE_EXT) write --skip-generated ./table/*.go
./bin/golangci-lint$(EXE_EXT):
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b ./bin v2.3.1
./bin/gci$(EXE_EXT):
GOBIN=$(shell pwd)/bin go install github.com/daixiang0/gci@v0.9.1
================================================
FILE: README.md
================================================
# Bubble-table
<p>
<a href="https://github.com/Evertras/bubble-table/releases"><img src="https://img.shields.io/github/release/Evertras/bubble-table.svg" alt="Latest Release"></a>
<a href="https://pkg.go.dev/github.com/evertras/bubble-table/table?tab=doc"><img src="https://godoc.org/github.com/golang/gddo?status.svg" alt="GoDoc"></a>
<a href='https://coveralls.io/github/Evertras/bubble-table?branch=main'><img src='https://coveralls.io/repos/github/Evertras/bubble-table/badge.svg?branch=main&hash=abc' alt='Coverage Status'/></a>
<a href='https://goreportcard.com/report/github.com/evertras/bubble-table'><img src='https://goreportcard.com/badge/github.com/evertras/bubble-table' alt='Go Report Card' /></a>
</p>
A customizable, interactive table component for the
[Bubble Tea framework](https://github.com/charmbracelet/bubbletea).

[View above sample source code](./examples/pokemon)
## Contributing
Contributions welcome, please [check the contributions doc](./CONTRIBUTING.md)
for a few helpful tips!
## Features
For a code reference of most available features, please see the [full feature example](./examples/features).
If you want to get started with a simple default table, [check the simplest example](./examples/simplest).
Displays a table with a header, rows, footer, and borders. The header can be
hidden, and the footer can be set to automatically show page information, use
custom text, or be hidden by default.
Columns can be fixed-width [or flexible width](./examples/flex). A maximum
width can be specified which enables [horizontal scrolling](./examples/scrolling),
and left-most columns can be frozen for easier reference.
Border shape is customizable with a basic thick square default. The color can
be modified by applying a base style with `lipgloss.NewStyle().BorderForeground(...)`.
Styles can be applied globally and to columns, rows, and individual cells.
The base style is applied first, then column, then row, then cell when
determining overrides. The default base style is a basic right-alignment.
[See the main feature example](./examples/features) to see styles and
how they override each other.
Styles can also be applied via a style function which can be used to apply
zebra striping, data-specific formatting, etc.
Can be focused to highlight a row and navigate with up/down (and j/k). These
keys can be customized with a KeyMap.
Can make rows selectable, and fetch the current selections.
Events can be checked for user interactions.
Pagination can be set with a given page size, which automatically generates a
simple footer to show the current page and total pages.
Built-in filtering can be enabled by setting any columns as filterable, using
a text box in the footer and `/` (customizable by keybind) to start filtering.
A missing indicator can be supplied to show missing data in rows.
Columns can be sorted in either ascending or descending order. Multiple columns
can be specified in a row. If multiple columns are specified, first the table
is sorted by the first specified column, then each group within that column is
sorted in smaller and smaller groups. [See the sorting example](examples/sorting)
for more information. If a column contains numbers (either ints or floats),
the numbers will be sorted by numeric value. Otherwise rendered string values
will be compared.
If a feature is confusing to use or could use a better example, please feel free
to open an issue.
## Defining table data
A table is defined by a list of `Column` values that define the columns in the
table. Each `Column` is associated with a unique string key.
A table contains a list of `Row`s. Each `Row` contains a `RowData` object which
is simply a map of string column IDs to arbitrary `any` data values.
When the table is rendered, each `Row` is checked for each `Column` key. If the
key exists in the `Row`'s `RowData`, it is rendered with `fmt.Sprintf("%v")`.
If it does not exist, nothing is rendered.
Extra data in the `RowData` object is ignored. This can be helpful to simply
dump data into `RowData` and create columns that select what is interesting to
view, or to generate different columns based on view options on the fly (see the
[metadata example](./examples/metadata) for an example of using this).
An example is given below. For more detailed examples, see
[the examples directory](./examples).
```golang
// This makes it easier/safer to match against values, but isn't necessary
const (
// This value isn't visible anywhere, so a simple lowercase is fine
columnKeyID = "id"
// It's just a string, so it can be whatever, really! They only must be unique
columnKeyName = "何?!"
)
// Note that there's nothing special about "ID" or "Name", these are completely
// arbitrary columns
columns := []table.Column{
table.NewColumn(columnKeyID, "ID", 5),
table.NewColumn(columnKeyName, "Name", 10),
}
rows := []table.Row{
// This row contains both an ID and a name
table.NewRow(table.RowData{
columnKeyID: "abc",
columnKeyName: "Hello",
}),
table.NewRow(table.RowData{
columnKeyID: "123",
columnKeyName: "Oh no",
// This field exists in the row data but won't be visible
"somethingelse": "Super bold!",
}),
table.NewRow(table.RowData{
columnKeyID: "def",
// This row is missing the Name column, so it will use the supplied missing
// indicator if supplied when creating the table using the following option:
// .WithMissingDataIndicator("<ない>") (or .WithMissingDataIndicatorStyled!)
}),
// We can also apply styling to the row or to individual cells
// This row has individual styling to make it bold
table.NewRow(table.RowData{
columnKeyID: "bold",
columnKeyName: "Bolded",
}).WithStyle(lipgloss.NewStyle().Bold(true). ,
// This row also has individual styling to make it bold
table.NewRow(table.RowData{
columnKeyID: "alert",
// This cell has styling applied on top of the bold
columnKeyName: table.NewStyledCell("Alert", lipgloss.NewStyle().Foreground(lipgloss.Color("#f88"))),
}).WithStyle(lipgloss.NewStyle().Bold(true),
}
```
### A note on 'metadata'
There may be cases where you wish to reference some kind of data object in the
table. For example, a table of users may display a user name, ID, etc., and you
may wish to retrieve data about the user when the row is selected. This can be
accomplished by attaching hidden 'metadata' to the row in the same way as any
other data.
```golang
const (
columnKeyID = "id"
columnKeyName = "名前"
columnKeyUserData = "userstuff"
)
// Notice there is no "userstuff" column, so it won't be displayed
columns := []table.Column{
table.NewColumn(columnKeyID, "ID", 5),
table.NewColumn(columnKeyName, "Name", 10),
}
// Just one user for this quick snippet, check the example for more
user := &SomeUser{
ID: 3,
Name: "Evertras",
}
rows := []table.Row{
// This row contains both an ID and a name
table.NewRow(table.RowData{
columnKeyID: user.ID,
columnKeyName: user.Name,
// This isn't displayed, but it remains attached to the row
columnKeyUserData: user,
}),
}
```
For a more detailed demonstration of this idea in action, please see the
[metadata example](./examples/metadata).
## Demos
Code examples are located in [the examples directory](./examples). Run commands
are added to the [Makefile](Makefile) for convenience but they should be as
simple as `go run ./examples/features/main.go`, etc. You can also view what
they look like by checking the example's directory in each README here on
Github.
To run the examples, clone this repo and run:
```bash
# Run the pokemon demo for a general feel of common useful features
make
# Run dimensions example to see multiple sizes of simple tables in action
make example-dimensions
# Or run any of them directly
go run ./examples/pagination/main.go
```
================================================
FILE: examples/dimensions/README.md
================================================
# Dimensions
Shows some simple tables with various dimensions.
<img width="534" alt="image" src="https://user-images.githubusercontent.com/5923958/170801679-92f420d7-5cdc-4c66-8d32-e421b1f5dc18.png">
================================================
FILE: examples/dimensions/main.go
================================================
package main
import (
"fmt"
"log"
"strings"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/evertras/bubble-table/table"
)
type Model struct {
table3x3 table.Model
table1x3 table.Model
table3x1 table.Model
table1x1 table.Model
table5x5 table.Model
}
func genTable(columnCount int, rowCount int) table.Model {
columns := []table.Column{}
for column := 0; column < columnCount; column++ {
columnStr := fmt.Sprintf("%d", column+1)
columns = append(columns, table.NewColumn(columnStr, columnStr, 4))
}
rows := []table.Row{}
for row := 1; row < rowCount; row++ {
rowData := table.RowData{}
for column := 0; column < columnCount; column++ {
columnStr := fmt.Sprintf("%d", column+1)
rowData[columnStr] = fmt.Sprintf("%d,%d", column+1, row+1)
}
rows = append(rows, table.NewRow(rowData))
}
return table.New(columns).WithRows(rows).HeaderStyle(lipgloss.NewStyle().Bold(true))
}
func NewModel() Model {
return Model{
table1x1: genTable(1, 1),
table3x1: genTable(3, 1),
table1x3: genTable(1, 3),
table3x3: genTable(3, 3),
table5x5: genTable(5, 5),
}
}
func (m Model) Init() tea.Cmd {
return nil
}
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var (
cmd tea.Cmd
cmds []tea.Cmd
)
m.table1x1, cmd = m.table1x1.Update(msg)
cmds = append(cmds, cmd)
m.table3x1, cmd = m.table3x1.Update(msg)
cmds = append(cmds, cmd)
m.table1x3, cmd = m.table1x3.Update(msg)
cmds = append(cmds, cmd)
m.table3x3, cmd = m.table3x3.Update(msg)
cmds = append(cmds, cmd)
m.table5x5, cmd = m.table5x5.Update(msg)
cmds = append(cmds, cmd)
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "esc", "q":
cmds = append(cmds, tea.Quit)
}
}
return m, tea.Batch(cmds...)
}
func (m Model) View() string {
body := strings.Builder{}
body.WriteString("Table demo with various sized tables!\nPress q or ctrl+c to quit\n")
pad := lipgloss.NewStyle().Padding(1)
tablesSmall := lipgloss.JoinHorizontal(
lipgloss.Top,
pad.Render(m.table1x1.View()),
pad.Render(m.table1x3.View()),
pad.Render(m.table3x1.View()),
pad.Render(m.table3x3.View()),
)
tableBig := pad.Render(m.table5x5.View())
body.WriteString(lipgloss.JoinVertical(lipgloss.Center, tablesSmall, tableBig))
return body.String()
}
func main() {
p := tea.NewProgram(NewModel())
if err := p.Start(); err != nil {
log.Fatal(err)
}
}
================================================
FILE: examples/events/README.md
================================================
# Events example
This example shows how to use events to handle triggers for data retrieval or
other desired behavior. This example in particular shows how to use events to
trigger data retrieval.
<img width="290" alt="image" src="https://user-images.githubusercontent.com/5923958/173168499-836bd03b-debb-455e-9f73-f2bfd028f2b2.png">
================================================
FILE: examples/events/main.go
================================================
package main
import (
"fmt"
"log"
"time"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/evertras/bubble-table/table"
)
type Element string
const (
columnKeyName = "name"
columnKeyElement = "element"
// This is not a visible column, but is used to attach useful reference data
// to the row itself for easier retrieval
columnKeyPokemonData = "pokedata"
elementNormal Element = "Normal"
elementFire Element = "Fire"
elementElectric Element = "Electric"
elementWater Element = "Water"
elementPlant Element = "Plant"
)
var (
styleSubtle = lipgloss.NewStyle().Foreground(lipgloss.Color("#888"))
styleBase = lipgloss.NewStyle().
Foreground(lipgloss.Color("#a7a")).
BorderForeground(lipgloss.Color("#a38")).
Align(lipgloss.Right)
elementColors = map[Element]string{
elementNormal: "#fa0",
elementFire: "#f64",
elementElectric: "#ff0",
elementWater: "#44f",
elementPlant: "#8b8",
}
)
type Pokemon struct {
Name string
Element Element
ConversationCount int
PositiveSentimentPercent float32
NegativeSentimentPercent float32
}
func NewPokemon(name string, element Element, conversationCount int, positiveSentimentPercent float32, negativeSentimentPercent float32) Pokemon {
return Pokemon{
Name: name,
Element: element,
ConversationCount: conversationCount,
PositiveSentimentPercent: positiveSentimentPercent,
NegativeSentimentPercent: negativeSentimentPercent,
}
}
func (p Pokemon) ToRow() table.Row {
color, exists := elementColors[p.Element]
if !exists {
color = elementColors[elementNormal]
}
return table.NewRow(table.RowData{
columnKeyName: p.Name,
columnKeyElement: table.NewStyledCell(p.Element, lipgloss.NewStyle().Foreground(lipgloss.Color(color))),
// This isn't a visible column, but we can add the data here anyway for later retrieval
columnKeyPokemonData: p,
})
}
type Model struct {
pokeTable table.Model
currentPokemonData Pokemon
lastSelectedEvent table.UserEventRowSelectToggled
}
func NewModel() Model {
pokemon := []Pokemon{
NewPokemon("Pikachu", elementElectric, 2300648, 21.9, 8.54),
NewPokemon("Eevee", elementNormal, 636373, 26.4, 7.37),
NewPokemon("Bulbasaur", elementPlant, 352190, 25.7, 9.02),
NewPokemon("Squirtle", elementWater, 241259, 25.6, 5.96),
NewPokemon("Blastoise", elementWater, 162794, 19.5, 6.04),
NewPokemon("Charmander", elementFire, 265760, 31.2, 5.25),
NewPokemon("Charizard", elementFire, 567763, 25.6, 7.56),
}
rows := []table.Row{}
for _, p := range pokemon {
rows = append(rows, p.ToRow())
}
return Model{
pokeTable: table.New([]table.Column{
table.NewColumn(columnKeyName, "Name", 13),
table.NewColumn(columnKeyElement, "Element", 10),
}).WithRows(rows).
BorderRounded().
WithBaseStyle(styleBase).
WithPageSize(4).
Focused(true).
SelectableRows(true),
currentPokemonData: pokemon[0],
}
}
func (m Model) Init() tea.Cmd {
return nil
}
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var (
cmd tea.Cmd
cmds []tea.Cmd
)
m.pokeTable, cmd = m.pokeTable.Update(msg)
cmds = append(cmds, cmd)
for _, e := range m.pokeTable.GetLastUpdateUserEvents() {
switch e := e.(type) {
case table.UserEventHighlightedIndexChanged:
// We can pretend this is an async data retrieval, but really we already
// have the data, so just return it after some fake delay. Also note
// that the event has some data attached to it, but we're ignoring
// that for this example as we just want the current highlighted row.
selectedPokemon := m.pokeTable.HighlightedRow().Data[columnKeyPokemonData].(Pokemon)
cmds = append(cmds, func() tea.Msg {
time.Sleep(time.Millisecond * 200)
return selectedPokemon
})
case table.UserEventRowSelectToggled:
m.lastSelectedEvent = e
}
}
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "esc", "q":
cmds = append(cmds, tea.Quit)
}
case Pokemon:
m.currentPokemonData = msg
}
return m, tea.Batch(cmds...)
}
func (m Model) View() string {
view := lipgloss.JoinVertical(
lipgloss.Left,
styleSubtle.Render("Press q or ctrl+c to quit"),
fmt.Sprintf("Highlighted (200 ms delay): %s (%s)", m.currentPokemonData.Name, m.currentPokemonData.Element),
fmt.Sprintf("Last selected event: %d (%v)", m.lastSelectedEvent.RowIndex, m.lastSelectedEvent.IsSelected),
lipgloss.NewStyle().Foreground(lipgloss.Color("#8c8")).
Render(":D %"+fmt.Sprintf("%.1f", m.currentPokemonData.PositiveSentimentPercent)),
lipgloss.NewStyle().Foreground(lipgloss.Color("#c88")).
Render(":( %"+fmt.Sprintf("%.1f", m.currentPokemonData.NegativeSentimentPercent)),
m.pokeTable.View(),
) + "\n"
return lipgloss.NewStyle().MarginLeft(1).Render(view)
}
func main() {
p := tea.NewProgram(NewModel())
if err := p.Start(); err != nil {
log.Fatal(err)
}
}
================================================
FILE: examples/features/README.md
================================================
# Full feature example
This table contains most of the implemented features, but as more features have
been added some have not made it into this demo for practical purposes. This
can be a useful reference for various features even if it's not a very
appealing final product.
<img width="593" alt="image" src="https://user-images.githubusercontent.com/5923958/170802611-84d15b28-0c57-4095-a321-d6ba05af58f8.png">
================================================
FILE: examples/features/main.go
================================================
// This file contains a full demo of most available features, for both testing
// and for reference
package main
import (
"fmt"
"log"
"strings"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/evertras/bubble-table/table"
)
const (
columnKeyID = "id"
columnKeyName = "name"
columnKeyDescription = "description"
columnKeyCount = "count"
)
var (
customBorder = table.Border{
Top: "─",
Left: "│",
Right: "│",
Bottom: "─",
TopRight: "╮",
TopLeft: "╭",
BottomRight: "╯",
BottomLeft: "╰",
TopJunction: "╥",
LeftJunction: "├",
RightJunction: "┤",
BottomJunction: "╨",
InnerJunction: "╫",
InnerDivider: "║",
}
)
type Model struct {
tableModel table.Model
}
func NewModel() Model {
columns := []table.Column{
table.NewColumn(columnKeyID, "ID", 5).WithStyle(
lipgloss.NewStyle().
Faint(true).
Foreground(lipgloss.Color("#88f")).
Align(lipgloss.Center)),
table.NewColumn(columnKeyName, "Name", 10),
table.NewColumn(columnKeyDescription, "Description", 30),
table.NewColumn(columnKeyCount, "#", 5),
}
rows := []table.Row{
table.NewRow(table.RowData{
columnKeyID: "abc",
// Missing name
columnKeyDescription: "The first table entry, ever",
columnKeyCount: 4,
}),
table.NewRow(table.RowData{
columnKeyID: "123",
columnKeyName: "Oh no",
columnKeyDescription: "Super bold!",
columnKeyCount: 17,
}).WithStyle(lipgloss.NewStyle().Foreground(lipgloss.Color("9")).Bold(true)),
table.NewRow(table.RowData{
columnKeyID: "def",
// Apply a style to this cell
columnKeyName: table.NewStyledCell("Styled", lipgloss.NewStyle().Foreground(lipgloss.Color("#8ff"))),
columnKeyDescription: "This is a really, really, really long description that will get cut off",
columnKeyCount: table.NewStyledCell(0, lipgloss.NewStyle().Faint(true)),
}),
table.NewRow(table.RowData{
columnKeyID: "spg",
columnKeyName: "Page 2",
columnKeyDescription: "Second page",
columnKeyCount: 2,
}),
table.NewRow(table.RowData{
columnKeyID: "spg2",
columnKeyName: "Page 2.1",
columnKeyDescription: "Second page again",
columnKeyCount: 4,
}),
}
// Start with the default key map and change it slightly, just for demoing
keys := table.DefaultKeyMap()
keys.RowDown.SetKeys("j", "down", "s")
keys.RowUp.SetKeys("k", "up", "w")
model := Model{
// Throw features in... the point is not to look good, it's just reference!
tableModel: table.New(columns).
WithRows(rows).
HeaderStyle(lipgloss.NewStyle().Foreground(lipgloss.Color("10")).Bold(true)).
SelectableRows(true).
Focused(true).
Border(customBorder).
WithKeyMap(keys).
WithStaticFooter("Footer!").
WithPageSize(3).
WithSelectedText(" ", "✓").
WithBaseStyle(
lipgloss.NewStyle().
BorderForeground(lipgloss.Color("#a38")).
Foreground(lipgloss.Color("#a7a")).
Align(lipgloss.Left),
).
SortByAsc(columnKeyID).
WithMissingDataIndicatorStyled(table.StyledCell{
Style: lipgloss.NewStyle().Foreground(lipgloss.Color("#faa")),
Data: "<ない>",
}),
}
model.updateFooter()
return model
}
func (m Model) Init() tea.Cmd {
return nil
}
func (m *Model) updateFooter() {
highlightedRow := m.tableModel.HighlightedRow()
footerText := fmt.Sprintf(
"Pg. %d/%d - Currently looking at ID: %s",
m.tableModel.CurrentPage(),
m.tableModel.MaxPages(),
highlightedRow.Data[columnKeyID],
)
m.tableModel = m.tableModel.WithStaticFooter(footerText)
}
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var (
cmd tea.Cmd
cmds []tea.Cmd
)
m.tableModel, cmd = m.tableModel.Update(msg)
cmds = append(cmds, cmd)
// We control the footer text, so make sure to update it
m.updateFooter()
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "esc", "q":
cmds = append(cmds, tea.Quit)
case "i":
m.tableModel = m.tableModel.WithHeaderVisibility(!m.tableModel.GetHeaderVisibility())
}
}
return m, tea.Batch(cmds...)
}
func (m Model) View() string {
body := strings.Builder{}
body.WriteString("A (chaotic) table demo with all features enabled!\n")
body.WriteString("Press left/right or page up/down to move pages\n")
body.WriteString("Press 'i' to toggle the header visibility\n")
body.WriteString("Press space/enter to select a row, q or ctrl+c to quit\n")
selectedIDs := []string{}
for _, row := range m.tableModel.SelectedRows() {
// Slightly dangerous type assumption but fine for demo
selectedIDs = append(selectedIDs, row.Data[columnKeyID].(string))
}
body.WriteString(fmt.Sprintf("SelectedIDs: %s\n", strings.Join(selectedIDs, ", ")))
body.WriteString(m.tableModel.View())
body.WriteString("\n")
return body.String()
}
func main() {
p := tea.NewProgram(NewModel())
if err := p.Start(); err != nil {
log.Fatal(err)
}
}
================================================
FILE: examples/filter/README.md
================================================
# Filter example
Shows how the table can use a built-in filter to filter results.
<img width="1052" alt="image" src="https://user-images.githubusercontent.com/5923958/170801744-a6488a8b-54f7-4132-b2c3-0e0d7166d902.png">
================================================
FILE: examples/filter/main.go
================================================
package main
import (
"log"
"strings"
tea "github.com/charmbracelet/bubbletea"
"github.com/evertras/bubble-table/table"
)
const (
columnKeyTitle = "title"
columnKeyAuthor = "author"
columnKeyDescription = "description"
)
type Model struct {
table table.Model
}
func NewModel() Model {
columns := []table.Column{
table.NewColumn(columnKeyTitle, "Title", 13).WithFiltered(true),
table.NewColumn(columnKeyAuthor, "Author", 13).WithFiltered(true),
table.NewColumn(columnKeyDescription, "Description", 50),
}
return Model{
table: table.
New(columns).
Filtered(true).
Focused(true).
WithPageSize(10).
SelectableRows(true).
WithRows([]table.Row{
table.NewRow(table.RowData{
columnKeyTitle: "Computer Systems : A Programmer's Perspective",
columnKeyAuthor: "Randal E. Bryant、David R. O'Hallaron / Prentice Hall ",
columnKeyDescription: "This book explains the important and enduring concepts underlying all computer...",
}),
table.NewRow(table.RowData{
columnKeyTitle: "Effective Java : 3rd Edition",
columnKeyAuthor: "Joshua Bloch",
columnKeyDescription: "The Definitive Guide to Java Platform Best Practices—Updated for Java 9 Java ...",
}),
table.NewRow(table.RowData{
columnKeyTitle: "Structure and Interpretation of Computer Programs - 2nd Edition (MIT)",
columnKeyAuthor: "Harold Abelson、Gerald Jay Sussman",
columnKeyDescription: "Structure and Interpretation of Computer Programs has had a dramatic impact on...",
}),
table.NewRow(table.RowData{
columnKeyTitle: "Game Programming Patterns",
columnKeyAuthor: "Robert Nystrom / Genever Benning",
columnKeyDescription: "The biggest challenge facing many game programmers is completing their game. M...",
}),
}),
}
}
func (m Model) Init() tea.Cmd {
return nil
}
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var (
cmd tea.Cmd
cmds []tea.Cmd
)
m.table, cmd = m.table.Update(msg)
cmds = append(cmds, cmd)
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "q":
if !m.table.GetIsFilterInputFocused() {
cmds = append(cmds, tea.Quit)
}
}
}
return m, tea.Batch(cmds...)
}
func (m Model) View() string {
body := strings.Builder{}
body.WriteString("A filtered simple default table\n" +
"Currently filter by Title and Author, press / + letters to start filtering, and escape to clear filter.\nPress q or ctrl+c to quit\n\n")
body.WriteString(m.table.View())
return body.String()
}
func main() {
p := tea.NewProgram(NewModel())
if err := p.Start(); err != nil {
log.Fatal(err)
}
}
================================================
FILE: examples/filterapi/README.md
================================================
# Filter API example
Similar to the [regular filter example](../filter/), but uses an external text
box control for filtering. This demonstrates how you can use your own text
controls to perform filtering for more flexible UIs.
<img width="1052" alt="image" src="https://user-images.githubusercontent.com/5923958/170801860-9bfac90e-00aa-4591-8799-00f67836e6b8.png">
================================================
FILE: examples/filterapi/main.go
================================================
package main
import (
"github.com/charmbracelet/bubbles/textinput"
"log"
"strings"
tea "github.com/charmbracelet/bubbletea"
"github.com/evertras/bubble-table/table"
)
const (
columnKeyTitle = "title"
columnKeyAuthor = "author"
columnKeyDescription = "description"
)
type Model struct {
table table.Model
filterTextInput textinput.Model
}
func NewModel() Model {
columns := []table.Column{
table.NewColumn(columnKeyTitle, "Title", 13).WithFiltered(true),
table.NewColumn(columnKeyAuthor, "Author", 13).WithFiltered(true),
table.NewColumn(columnKeyDescription, "Description", 50),
}
return Model{
table: table.
New(columns).
Filtered(true).
Focused(true).
WithFooterVisibility(false).
WithPageSize(10).
WithRows([]table.Row{
table.NewRow(table.RowData{
columnKeyTitle: "Computer Systems : A Programmer's Perspective",
columnKeyAuthor: "Randal E. Bryant、David R. O'Hallaron / Prentice Hall ",
columnKeyDescription: "This book explains the important and enduring concepts underlying all computer...",
}),
table.NewRow(table.RowData{
columnKeyTitle: "Effective Java : 3rd Edition",
columnKeyAuthor: "Joshua Bloch",
columnKeyDescription: "The Definitive Guide to Java Platform Best Practices—Updated for Java 9 Java ...",
}),
table.NewRow(table.RowData{
columnKeyTitle: "Structure and Interpretation of Computer Programs - 2nd Edition (MIT)",
columnKeyAuthor: "Harold Abelson、Gerald Jay Sussman",
columnKeyDescription: "Structure and Interpretation of Computer Programs has had a dramatic impact on...",
}),
table.NewRow(table.RowData{
columnKeyTitle: "Game Programming Patterns",
columnKeyAuthor: "Robert Nystrom / Genever Benning",
columnKeyDescription: "The biggest challenge facing many game programmers is completing their game. M...",
}),
}),
filterTextInput: textinput.New(),
}
}
func (m Model) Init() tea.Cmd {
return nil
}
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var (
cmd tea.Cmd
cmds []tea.Cmd
)
switch msg := msg.(type) {
case tea.KeyMsg:
// global
if msg.String() == "ctrl+c" {
cmds = append(cmds, tea.Quit)
return m, tea.Batch(cmds...)
}
// event to filter
if m.filterTextInput.Focused() {
if msg.String() == "enter" {
m.filterTextInput.Blur()
} else {
m.filterTextInput, _ = m.filterTextInput.Update(msg)
}
m.table = m.table.WithFilterInput(m.filterTextInput)
return m, tea.Batch(cmds...)
}
// others component
switch msg.String() {
case "/":
m.filterTextInput.Focus()
case "q":
cmds = append(cmds, tea.Quit)
return m, tea.Batch(cmds...)
default:
m.table, cmd = m.table.Update(msg)
cmds = append(cmds, cmd)
}
}
return m, tea.Batch(cmds...)
}
func (m Model) View() string {
body := strings.Builder{}
body.WriteString("A filtered simple default table\n" +
"Currently filter by Title and Author, press / + letters to start filtering, and escape to clear filter.\n" +
"Press q or ctrl+c to quit\n\n")
body.WriteString(m.filterTextInput.View() + "\n")
body.WriteString(m.table.View())
return body.String()
}
func main() {
p := tea.NewProgram(NewModel())
if err := p.Start(); err != nil {
log.Fatal(err)
}
}
================================================
FILE: examples/flex/README.md
================================================
# Flex example
This example shows how to use flexible-width columns. The example stretches to
fill the full width of the terminal whenever it's resized.
<img width="904" alt="image" src="https://user-images.githubusercontent.com/5923958/170801927-36599e10-7d25-4b8f-94d1-7052c25f572d.png">
================================================
FILE: examples/flex/main.go
================================================
package main
import (
"fmt"
"log"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/evertras/bubble-table/table"
)
const (
columnKeyName = "name"
columnKeyElement = "element"
columnKeyDescription = "description"
minWidth = 30
minHeight = 8
// Add a fixed margin to account for description & instructions
fixedVerticalMargin = 4
)
type Model struct {
flexTable table.Model
// Window dimensions
totalWidth int
totalHeight int
// Table dimensions
horizontalMargin int
verticalMargin int
}
func NewModel() Model {
return Model{
flexTable: table.New([]table.Column{
table.NewColumn(columnKeyName, "Name", 10),
// This table uses flex columns, but it will still need a target
// width in order to know what width it should fill. In this example
// the target width is set below in `recalculateTable`, which sets
// the table to the width of the screen to demonstrate resizing
// with flex columns.
table.NewFlexColumn(columnKeyElement, "Element", 1),
table.NewFlexColumn(columnKeyDescription, "Description", 3),
}).WithRows([]table.Row{
table.NewRow(table.RowData{
columnKeyName: "Pikachu",
columnKeyElement: "Electric",
columnKeyDescription: "Super zappy mouse, handle with care",
}),
table.NewRow(table.RowData{
columnKeyName: "Charmander",
columnKeyElement: "Fire",
columnKeyDescription: "直立した恐竜のような身体と、尻尾の先端に常に燃えている炎が特徴。",
}),
}).WithStaticFooter("A footer!"),
}
}
func (m Model) Init() tea.Cmd {
return nil
}
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var (
cmd tea.Cmd
cmds []tea.Cmd
)
m.flexTable, cmd = m.flexTable.Update(msg)
cmds = append(cmds, cmd)
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "esc", "q":
cmds = append(cmds, tea.Quit)
case "left":
if m.calculateWidth() > minWidth {
m.horizontalMargin++
m.recalculateTable()
}
case "right":
if m.horizontalMargin > 0 {
m.horizontalMargin--
m.recalculateTable()
}
case "up":
if m.calculateHeight() > minHeight {
m.verticalMargin++
m.recalculateTable()
}
case "down":
if m.verticalMargin > 0 {
m.verticalMargin--
m.recalculateTable()
}
}
case tea.WindowSizeMsg:
m.totalWidth = msg.Width
m.totalHeight = msg.Height
m.recalculateTable()
}
return m, tea.Batch(cmds...)
}
func (m *Model) recalculateTable() {
m.flexTable = m.flexTable.
WithTargetWidth(m.calculateWidth()).
WithMinimumHeight(m.calculateHeight())
}
func (m Model) calculateWidth() int {
return m.totalWidth - m.horizontalMargin
}
func (m Model) calculateHeight() int {
return m.totalHeight - m.verticalMargin - fixedVerticalMargin
}
func (m Model) View() string {
strs := []string{
"A flexible table that fills available space (Name column is fixed-width)",
fmt.Sprintf("Target size: %d W ⨉ %d H (arrow keys to adjust)",
m.calculateWidth(), m.calculateHeight()),
"Press q or ctrl+c to quit",
m.flexTable.View(),
}
return lipgloss.JoinVertical(lipgloss.Left, strs...) + "\n"
}
func main() {
p := tea.NewProgram(NewModel())
if err := p.Start(); err != nil {
log.Fatal(err)
}
}
================================================
FILE: examples/metadata/README.md
================================================
# Metadata example
This is the [pokemon example](../pokemon) with metadata attached to the rows in
order to retrieve data, instead of retrieving the data directly from the row.
This can be a useful technique to make more natural data transformations.

================================================
FILE: examples/metadata/main.go
================================================
// This is a more data-driven example of the Pokemon table
package main
import (
"fmt"
"log"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/evertras/bubble-table/table"
)
type Element string
const (
columnKeyName = "name"
columnKeyElement = "element"
columnKeyConversations = "convos"
columnKeyPositiveSentiment = "positive"
columnKeyNegativeSentiment = "negative"
// This is not a visible column, but is used to attach useful reference data
// to the row itself for easier retrieval
columnKeyPokemonData = "pokedata"
elementNormal Element = "Normal"
elementFire Element = "Fire"
elementElectric Element = "Electric"
elementWater Element = "Water"
elementPlant Element = "Plant"
)
var (
styleSubtle = lipgloss.NewStyle().Foreground(lipgloss.Color("#888"))
styleBase = lipgloss.NewStyle().
Foreground(lipgloss.Color("#a7a")).
BorderForeground(lipgloss.Color("#a38")).
Align(lipgloss.Right)
elementColors = map[Element]string{
elementNormal: "#fa0",
elementFire: "#f64",
elementElectric: "#ff0",
elementWater: "#44f",
elementPlant: "#8b8",
}
)
type Pokemon struct {
Name string
Element Element
ConversationCount int
PositiveSentimentPercent float32
NegativeSentimentPercent float32
}
func NewPokemon(name string, element Element, conversationCount int, positiveSentimentPercent float32, negativeSentimentPercent float32) Pokemon {
return Pokemon{
Name: name,
Element: element,
ConversationCount: conversationCount,
PositiveSentimentPercent: positiveSentimentPercent,
NegativeSentimentPercent: negativeSentimentPercent,
}
}
func (p Pokemon) ToRow() table.Row {
color, exists := elementColors[p.Element]
if !exists {
color = elementColors[elementNormal]
}
return table.NewRow(table.RowData{
columnKeyName: p.Name,
columnKeyElement: table.NewStyledCell(p.Element, lipgloss.NewStyle().Foreground(lipgloss.Color(color))),
columnKeyConversations: p.ConversationCount,
columnKeyPositiveSentiment: p.PositiveSentimentPercent,
columnKeyNegativeSentiment: p.NegativeSentimentPercent,
// This isn't a visible column, but we can add the data here anyway for later retrieval
columnKeyPokemonData: p,
})
}
type Model struct {
pokeTable table.Model
}
func NewModel() Model {
pokemon := []Pokemon{
NewPokemon("Pikachu", elementElectric, 2300648, 21.9, 8.54),
NewPokemon("Eevee", elementNormal, 636373, 26.4, 7.37),
NewPokemon("Bulbasaur", elementPlant, 352190, 25.7, 9.02),
NewPokemon("Squirtle", elementWater, 241259, 25.6, 5.96),
NewPokemon("Blastoise", elementWater, 162794, 19.5, 6.04),
NewPokemon("Charmander", elementFire, 265760, 31.2, 5.25),
NewPokemon("Charizard", elementFire, 567763, 25.6, 7.56),
}
rows := []table.Row{}
for _, p := range pokemon {
rows = append(rows, p.ToRow())
}
return Model{
pokeTable: table.New([]table.Column{
table.NewColumn(columnKeyName, "Name", 13),
table.NewColumn(columnKeyElement, "Element", 10),
table.NewColumn(columnKeyConversations, "# Conversations", 15),
table.NewColumn(columnKeyPositiveSentiment, ":D %", 5).WithStyle(lipgloss.NewStyle().Foreground(lipgloss.Color("#8c8"))),
table.NewColumn(columnKeyNegativeSentiment, ":( %", 5).WithStyle(lipgloss.NewStyle().Foreground(lipgloss.Color("#c88"))),
}).WithRows(rows).
BorderRounded().
WithBaseStyle(styleBase).
WithPageSize(6).
SortByDesc(columnKeyConversations).
Focused(true),
}
}
func (m Model) Init() tea.Cmd {
return nil
}
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var (
cmd tea.Cmd
cmds []tea.Cmd
)
m.pokeTable, cmd = m.pokeTable.Update(msg)
cmds = append(cmds, cmd)
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "esc", "q":
cmds = append(cmds, tea.Quit)
}
}
return m, tea.Batch(cmds...)
}
func (m Model) View() string {
// Get the metadata back out of the row
selected := m.pokeTable.HighlightedRow().Data[columnKeyPokemonData].(Pokemon)
view := lipgloss.JoinVertical(
lipgloss.Left,
styleSubtle.Render("Press q or ctrl+c to quit - Sorted by # Conversations"),
styleSubtle.Render("Highlighted: "+fmt.Sprintf("%s (%s)", selected.Name, selected.Element)),
styleSubtle.Render("https://www.nintendolife.com/news/2021/11/these-are-the-most-loved-and-most-hated-pokemon-according-to-a-new-study"),
m.pokeTable.View(),
) + "\n"
return lipgloss.NewStyle().MarginLeft(1).Render(view)
}
func main() {
p := tea.NewProgram(NewModel())
if err := p.Start(); err != nil {
log.Fatal(err)
}
}
================================================
FILE: examples/multiline/README.md
================================================
# Multiline Example
This example code showcases the implementation of a multiline feature. The feature enables users to input and display content spanning multiple lines within the row. The provided code allows you to integrate the multiline feature seamlessly into your project. Feel free to experiment and adapt the code based on your specific requirements.
<img width="593" alt="image" src="https://github.com/Evertras/bubble-table/assets/23465248/3092b6f2-1e75-4c11-85f6-fcbea249d509">
================================================
FILE: examples/multiline/main.go
================================================
package main
import (
"log"
"strings"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/evertras/bubble-table/table"
)
const (
columnKeyName = "name"
columnKeyCountry = "country"
columnKeyCurrency = "crurrency"
)
type Model struct {
tableModel table.Model
}
func NewModel() Model {
columns := []table.Column{
table.NewColumn(columnKeyName, "Name", 10).WithStyle(
lipgloss.NewStyle().
Foreground(lipgloss.Color("#88f")),
),
table.NewColumn(columnKeyCountry, "Country", 20),
table.NewColumn(columnKeyCurrency, "Currency", 10),
}
rows := []table.Row{
table.NewRow(
table.RowData{
columnKeyName: "Talon Stokes",
columnKeyCountry: "Mexico",
columnKeyCurrency: "$23.17",
}),
table.NewRow(
table.RowData{
columnKeyName: "Sonia Shepard",
columnKeyCountry: "United States",
columnKeyCurrency: "$76.47",
}),
table.NewRow(
table.RowData{
columnKeyName: "Shad Reed",
columnKeyCountry: "Turkey",
columnKeyCurrency: "$62.99",
}),
table.NewRow(
table.RowData{
columnKeyName: "Kibo Clay",
columnKeyCountry: "Philippines",
columnKeyCurrency: "$29.82",
}),
table.NewRow(
table.RowData{
columnKeyName: "Leslie Kerr",
columnKeyCountry: "Singapore",
columnKeyCurrency: "$70.54",
}),
table.NewRow(
table.RowData{
columnKeyName: "Micah Hurst",
columnKeyCountry: "Pakistan",
columnKeyCurrency: "$80.84",
}),
table.NewRow(
table.RowData{
columnKeyName: "Dora Miranda",
columnKeyCountry: "Colombia",
columnKeyCurrency: "$34.75",
}),
table.NewRow(
table.RowData{
columnKeyName: "Keefe Walters",
columnKeyCountry: "China",
columnKeyCurrency: "$56.82",
}),
table.NewRow(
table.RowData{
columnKeyName: "Fujimoto Tarokizaemon no shoutokinori",
columnKeyCountry: "Japan",
columnKeyCurrency: "$89.31",
}),
table.NewRow(
table.RowData{
columnKeyName: "Keefe Walters",
columnKeyCountry: "China",
columnKeyCurrency: "$56.82",
}),
table.NewRow(
table.RowData{
columnKeyName: "Vincent Sanchez",
columnKeyCountry: "Peru",
columnKeyCurrency: "$71.60",
}),
table.NewRow(
table.RowData{
columnKeyName: "Lani Figueroa",
columnKeyCountry: "United Kingdom",
columnKeyCurrency: "$90.67",
}),
}
model := Model{
tableModel: table.New(columns).
WithRows(rows).
HeaderStyle(lipgloss.NewStyle().Foreground(lipgloss.Color("10")).Bold(true)).
Focused(true).
WithBaseStyle(
lipgloss.NewStyle().
BorderForeground(lipgloss.Color("#a38")).
Foreground(lipgloss.Color("#a7a")).
Align(lipgloss.Left),
).
WithMultiline(true),
}
return model
}
func (m Model) Init() tea.Cmd {
return nil
}
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var (
cmd tea.Cmd
cmds []tea.Cmd
)
m.tableModel, cmd = m.tableModel.Update(msg)
cmds = append(cmds, cmd)
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "esc", "q":
cmds = append(cmds, tea.Quit)
}
}
return m, tea.Batch(cmds...)
}
func (m Model) View() string {
body := strings.Builder{}
body.WriteString("A table demo with multiline feature enabled!\n")
body.WriteString("Press up/down or j/k to move around\n")
body.WriteString(m.tableModel.View())
body.WriteString("\n")
return body.String()
}
func main() {
p := tea.NewProgram(NewModel())
if err := p.Start(); err != nil {
log.Fatal(err)
}
}
================================================
FILE: examples/pagination/README.md
================================================
# Pagination example
This example shows how to paginate data that would be too long to show in a
single screen. It also shows how to do this with multiple tables and to
navigate between them.
<img width="1131" alt="image" src="https://user-images.githubusercontent.com/5923958/170802030-6adcc324-7ee0-42c8-ac80-df1b0eb7d08b.png">
================================================
FILE: examples/pagination/main.go
================================================
package main
import (
"fmt"
"log"
"math/rand"
"strings"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/evertras/bubble-table/table"
)
type Model struct {
tableDefault table.Model
tableWithRowIndices table.Model
rowCount int
}
func genRows(columnCount int, rowCount int) []table.Row {
rows := []table.Row{}
for row := 1; row <= rowCount; row++ {
rowData := table.RowData{}
for column := 0; column < columnCount; column++ {
columnStr := fmt.Sprintf("%d", column+1)
rowData[columnStr] = fmt.Sprintf("%d - %d", column+1, row)
}
rows = append(rows, table.NewRow(rowData))
}
return rows
}
func genTable(columnCount int, rowCount int) table.Model {
columns := []table.Column{}
for column := 0; column < columnCount; column++ {
columnStr := fmt.Sprintf("%d", column+1)
columns = append(columns, table.NewColumn(columnStr, columnStr, 8))
}
rows := genRows(columnCount, rowCount)
return table.New(columns).WithRows(rows).HeaderStyle(lipgloss.NewStyle().Bold(true))
}
func NewModel() Model {
const startingRowCount = 105
m := Model{
rowCount: startingRowCount,
tableDefault: genTable(3, startingRowCount).WithPageSize(10).Focused(true),
tableWithRowIndices: genTable(3, startingRowCount).WithPageSize(10).Focused(false),
}
m.regenTableRows()
return m
}
func (m *Model) regenTableRows() {
m.tableDefault = m.tableDefault.WithRows(genRows(3, m.rowCount))
m.tableWithRowIndices = m.tableWithRowIndices.WithRows(genRows(3, m.rowCount))
}
func (m Model) Init() tea.Cmd {
return nil
}
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var (
cmd tea.Cmd
cmds []tea.Cmd
)
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "esc", "q":
cmds = append(cmds, tea.Quit)
case "a":
m.tableDefault = m.tableDefault.Focused(true)
m.tableWithRowIndices = m.tableWithRowIndices.Focused(false)
case "b":
m.tableDefault = m.tableDefault.Focused(false)
m.tableWithRowIndices = m.tableWithRowIndices.Focused(true)
case "u":
m.tableDefault = m.tableDefault.WithPageSize(m.tableDefault.PageSize() - 1)
m.tableWithRowIndices = m.tableWithRowIndices.WithPageSize(m.tableWithRowIndices.PageSize() - 1)
case "i":
m.tableDefault = m.tableDefault.WithPageSize(m.tableDefault.PageSize() + 1)
m.tableWithRowIndices = m.tableWithRowIndices.WithPageSize(m.tableWithRowIndices.PageSize() + 1)
case "r":
m.tableDefault = m.tableDefault.WithCurrentPage(rand.Intn(m.tableDefault.MaxPages()) + 1)
m.tableWithRowIndices = m.tableWithRowIndices.WithCurrentPage(rand.Intn(m.tableWithRowIndices.MaxPages()) + 1)
case "z":
if m.rowCount < 10 {
break
}
m.rowCount -= 10
m.regenTableRows()
case "x":
m.rowCount += 10
m.regenTableRows()
}
}
m.tableDefault, cmd = m.tableDefault.Update(msg)
cmds = append(cmds, cmd)
m.tableWithRowIndices, cmd = m.tableWithRowIndices.Update(msg)
cmds = append(cmds, cmd)
// Write a custom footer
start, end := m.tableWithRowIndices.VisibleIndices()
m.tableWithRowIndices = m.tableWithRowIndices.WithStaticFooter(
fmt.Sprintf("%d-%d of %d", start+1, end+1, m.tableWithRowIndices.TotalRows()),
)
return m, tea.Batch(cmds...)
}
func (m Model) View() string {
body := strings.Builder{}
body.WriteString("Table demo with pagination! Press left/right to move pages, or use page up/down, or 'r' to jump to a random page\nPress 'a' for left table, 'b' for right table\nPress 'z' to reduce rows by 10, 'y' to increase rows by 10\nPress 'u' to decrease page size by 1, 'i' to increase page size by 1\nPress q or ctrl+c to quit\n\n")
pad := lipgloss.NewStyle().Padding(1)
tables := []string{
lipgloss.JoinVertical(lipgloss.Center, "A", pad.Render(m.tableDefault.View())),
lipgloss.JoinVertical(lipgloss.Center, "B", pad.Render(m.tableWithRowIndices.View())),
}
body.WriteString(lipgloss.JoinHorizontal(lipgloss.Top, tables...))
return body.String()
}
func main() {
p := tea.NewProgram(NewModel())
if err := p.Start(); err != nil {
log.Fatal(err)
}
}
================================================
FILE: examples/pokemon/README.md
================================================
# Pokemon example
This example is a general use of the table using various features to nicely
display a table of Pokemon and some interesting statistics about them.

================================================
FILE: examples/pokemon/main.go
================================================
package main
import (
"log"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/evertras/bubble-table/table"
)
const (
columnKeyName = "name"
columnKeyElement = "element"
columnKeyConversations = "convos"
columnKeyPositiveSentiment = "positive"
columnKeyNegativeSentiment = "negative"
colorNormal = "#fa0"
colorElectric = "#ff0"
colorFire = "#f64"
colorPlant = "#8b8"
colorWater = "#44f"
)
var (
styleSubtle = lipgloss.NewStyle().Foreground(lipgloss.Color("#888"))
styleBase = lipgloss.NewStyle().
Foreground(lipgloss.Color("#a7a")).
BorderForeground(lipgloss.Color("#a38")).
Align(lipgloss.Right)
)
type Model struct {
pokeTable table.Model
favoriteElementIndex int
}
var elementList = []string{
"Normal",
"Electric",
"Fire",
"Plant",
"Water",
}
var colorMap = map[any]string{
"Electric": colorElectric,
"Fire": colorFire,
"Plant": colorPlant,
"Water": colorWater,
}
func makeRow(name, element string, numConversations int, positiveSentiment, negativeSentiment float32) table.Row {
elementStyleFunc := func(input table.StyledCellFuncInput) lipgloss.Style {
color := colorNormal
if val, ok := colorMap[input.Data]; ok {
color = val
}
style := lipgloss.NewStyle().Foreground(lipgloss.Color(color))
if input.GlobalMetadata["favoriteElement"] == input.Data {
style = style.Italic(true)
}
return style
}
return table.NewRow(table.RowData{
columnKeyName: name,
columnKeyElement: table.NewStyledCellWithStyleFunc(element, elementStyleFunc),
columnKeyConversations: numConversations,
columnKeyPositiveSentiment: positiveSentiment,
columnKeyNegativeSentiment: negativeSentiment,
})
}
func genMetadata(favoriteElementIndex int) map[string]any {
return map[string]any{
"favoriteElement": elementList[favoriteElementIndex],
}
}
func NewModel() Model {
initialFavoriteElementIndex := 0
return Model{
favoriteElementIndex: initialFavoriteElementIndex,
pokeTable: table.New([]table.Column{
table.NewColumn(columnKeyName, "Name", 13),
table.NewColumn(columnKeyElement, "Element", 10),
table.NewColumn(columnKeyConversations, "# Conversations", 15),
table.NewColumn(columnKeyPositiveSentiment, ":D %", 6).
WithStyle(lipgloss.NewStyle().Foreground(lipgloss.Color("#8c8"))).
WithFormatString("%.1f%%"),
table.NewColumn(columnKeyNegativeSentiment, ":( %", 6).
WithStyle(lipgloss.NewStyle().Foreground(lipgloss.Color("#c88"))).
WithFormatString("%.1f%%"),
}).WithRows([]table.Row{
makeRow("Pikachu", "Electric", 2300648, 21.9, 8.54),
makeRow("Eevee", "Normal", 636373, 26.4, 7.37),
makeRow("Bulbasaur", "Plant", 352190, 25.7, 9.02),
makeRow("Squirtle", "Water", 241259, 25.6, 5.96),
makeRow("Blastoise", "Water", 162794, 19.5, 6.04),
makeRow("Charmander", "Fire", 265760, 31.2, 5.25),
makeRow("Charizard", "Fire", 567763, 25.6, 7.56),
}).
BorderRounded().
WithBaseStyle(styleBase).
WithPageSize(6).
SortByDesc(columnKeyConversations).
Focused(true).
WithGlobalMetadata(genMetadata(initialFavoriteElementIndex)),
}
}
func (m Model) Init() tea.Cmd {
return nil
}
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var (
cmd tea.Cmd
cmds []tea.Cmd
)
m.pokeTable, cmd = m.pokeTable.Update(msg)
cmds = append(cmds, cmd)
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "esc", "q":
cmds = append(cmds, tea.Quit)
case "e":
m.favoriteElementIndex++
if m.favoriteElementIndex >= len(elementList) {
m.favoriteElementIndex = 0
}
m.pokeTable = m.pokeTable.WithGlobalMetadata(genMetadata(m.favoriteElementIndex))
}
}
return m, tea.Batch(cmds...)
}
func (m Model) View() string {
selected := m.pokeTable.HighlightedRow().Data[columnKeyName].(string)
view := lipgloss.JoinVertical(
lipgloss.Left,
styleSubtle.Render("Press q or ctrl+c to quit - Sorted by # Conversations"),
styleSubtle.Render("Highlighted: "+selected),
styleSubtle.Render("Favorite element: "+elementList[m.favoriteElementIndex]),
styleSubtle.Render("https://www.nintendolife.com/news/2021/11/these-are-the-most-loved-and-most-hated-pokemon-according-to-a-new-study"),
m.pokeTable.View(),
) + "\n"
return lipgloss.NewStyle().MarginLeft(1).Render(view)
}
func main() {
p := tea.NewProgram(NewModel())
if err := p.Start(); err != nil {
log.Fatal(err)
}
}
================================================
FILE: examples/scrolling/README.md
================================================
# Scrolling example
This example shows how to use scrolling to navigate particularly large tables
that may not fit nicely onto the screen.
<img width="423" alt="image" src="https://user-images.githubusercontent.com/5923958/172004548-7052993e-9e60-44a4-b9b2-b49506c48fb6.png">
================================================
FILE: examples/scrolling/main.go
================================================
package main
import (
"fmt"
"log"
"strings"
tea "github.com/charmbracelet/bubbletea"
"github.com/evertras/bubble-table/table"
)
const (
columnKeyID = "id"
numCols = 100
numRows = 10
idWidth = 5
colWidth = 3
maxWidth = 30
)
type Model struct {
scrollableTable table.Model
}
func colKey(colNum int) string {
return fmt.Sprintf("%d", colNum)
}
func genRow(id int) table.Row {
data := table.RowData{
columnKeyID: fmt.Sprintf("ID %d", id),
}
for i := 0; i < numCols; i++ {
data[colKey(i)] = colWidth
}
return table.NewRow(data)
}
func NewModel() Model {
rows := []table.Row{}
for i := 0; i < numRows; i++ {
rows = append(rows, genRow(i))
}
cols := []table.Column{
table.NewColumn(columnKeyID, "ID", idWidth),
}
for i := 0; i < numCols; i++ {
cols = append(cols, table.NewColumn(colKey(i), colKey(i+1), colWidth))
}
t := table.New(cols).
WithRows(rows).
WithMaxTotalWidth(maxWidth).
WithHorizontalFreezeColumnCount(1).
WithStaticFooter("A footer").
Focused(true)
return Model{
scrollableTable: t,
}
}
func (m Model) Init() tea.Cmd {
return nil
}
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var (
cmd tea.Cmd
cmds []tea.Cmd
)
m.scrollableTable, cmd = m.scrollableTable.Update(msg)
cmds = append(cmds, cmd)
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "esc", "q":
cmds = append(cmds, tea.Quit)
}
}
return m, tea.Batch(cmds...)
}
func (m Model) View() string {
body := strings.Builder{}
body.WriteString("A scrollable table\nPress shift+left or shift+right to scroll\nPress q or ctrl+c to quit\n\n")
body.WriteString(m.scrollableTable.View())
return body.String()
}
func main() {
p := tea.NewProgram(NewModel())
if err := p.Start(); err != nil {
log.Fatal(err)
}
}
================================================
FILE: examples/simplest/README.md
================================================
# Simplest example
This is a bare bones example of how to get started with the table component. It
uses all the defaults to simply present data, and is a good starting point for
other use cases.
<img width="462" alt="image" src="https://user-images.githubusercontent.com/5923958/170802181-f0c54b8b-625a-4ff0-8ffa-0e36cc2567eb.png">
================================================
FILE: examples/simplest/main.go
================================================
package main
import (
"log"
"strings"
tea "github.com/charmbracelet/bubbletea"
"github.com/evertras/bubble-table/table"
)
const (
columnKeyName = "name"
columnKeyElement = "element"
)
type Model struct {
simpleTable table.Model
}
func NewModel() Model {
return Model{
simpleTable: table.New([]table.Column{
table.NewColumn(columnKeyName, "Name", 13),
table.NewColumn(columnKeyElement, "Element", 10),
}).WithRows([]table.Row{
table.NewRow(table.RowData{
columnKeyName: "Pikachu",
columnKeyElement: "Electric",
}),
table.NewRow(table.RowData{
columnKeyName: "Charmander",
columnKeyElement: "Fire",
}),
}),
}
}
func (m Model) Init() tea.Cmd {
return nil
}
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var (
cmd tea.Cmd
cmds []tea.Cmd
)
m.simpleTable, cmd = m.simpleTable.Update(msg)
cmds = append(cmds, cmd)
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "esc", "q":
cmds = append(cmds, tea.Quit)
}
}
return m, tea.Batch(cmds...)
}
func (m Model) View() string {
body := strings.Builder{}
body.WriteString("A very simple default table (non-interactive)\nPress q or ctrl+c to quit\n\n")
body.WriteString(m.simpleTable.View())
return body.String()
}
func main() {
p := tea.NewProgram(NewModel())
if err := p.Start(); err != nil {
log.Fatal(err)
}
}
================================================
FILE: examples/sorting/README.md
================================================
# Sorting example
This example shows how to use the sorting feature, which sorts columns. It
demonstrates how numbers are numerically sorted while strings are alphabetically
sorted. It also demonstrates multi-column sorting to group different kinds of
data together and sort within them.
<img width="461" alt="image" src="https://user-images.githubusercontent.com/5923958/187064825-59d60dc7-cc75-4c24-bc3a-d653decdf700.png">
================================================
FILE: examples/sorting/main.go
================================================
package main
import (
"log"
"strings"
tea "github.com/charmbracelet/bubbletea"
"github.com/evertras/bubble-table/table"
)
const (
columnKeyName = "name"
columnKeyType = "type"
columnKeyWins = "wins"
)
type Model struct {
simpleTable table.Model
columnSortKey string
sortDirection string
}
func NewModel() Model {
return Model{
simpleTable: table.New([]table.Column{
table.NewColumn(columnKeyName, "Name", 13),
table.NewColumn(columnKeyType, "Type", 13),
table.NewColumn(columnKeyWins, "Win %", 8).
WithFormatString("%.1f%%"),
}).WithRows([]table.Row{
table.NewRow(table.RowData{
columnKeyName: "ピカピカ",
columnKeyType: "Pikachu",
columnKeyWins: 78.3,
}),
table.NewRow(table.RowData{
columnKeyName: "Zapmouse",
columnKeyType: "Pikachu",
columnKeyWins: 3.3,
}),
table.NewRow(table.RowData{
columnKeyName: "Burninator",
columnKeyType: "Charmander",
columnKeyWins: 32.1,
}),
table.NewRow(table.RowData{
columnKeyName: "Alphonse",
columnKeyType: "Pikachu",
columnKeyWins: 13.8,
}),
table.NewRow(table.RowData{
columnKeyName: "Trogdor",
columnKeyType: "Charmander",
columnKeyWins: 99.9,
}),
table.NewRow(table.RowData{
columnKeyName: "Dihydrogen Monoxide",
columnKeyType: "Squirtle",
columnKeyWins: 31.348,
}),
}),
}
}
func (m Model) Init() tea.Cmd {
return nil
}
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var (
cmd tea.Cmd
cmds []tea.Cmd
)
m.simpleTable, cmd = m.simpleTable.Update(msg)
cmds = append(cmds, cmd)
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "esc", "q":
cmds = append(cmds, tea.Quit)
case "n":
m.columnSortKey = columnKeyName
m.simpleTable = m.simpleTable.SortByAsc(m.columnSortKey)
case "t":
m.columnSortKey = columnKeyType
// Within the same type, order each by wins
m.simpleTable = m.simpleTable.SortByAsc(m.columnSortKey).ThenSortByDesc(columnKeyWins)
case "w":
m.columnSortKey = columnKeyWins
m.simpleTable = m.simpleTable.SortByDesc(m.columnSortKey)
}
}
return m, tea.Batch(cmds...)
}
func (m Model) View() string {
body := strings.Builder{}
body.WriteString("A sorted simple default table\nSort by (n)ame, (t)ype->wins combo, or (w)ins\nCurrently sorting by: " + m.columnSortKey + "\nPress q or ctrl+c to quit\n\n")
body.WriteString(m.simpleTable.View())
return body.String()
}
func main() {
p := tea.NewProgram(NewModel())
if err := p.Start(); err != nil {
log.Fatal(err)
}
}
================================================
FILE: examples/updates/README.md
================================================
# Update example
Shows how to update data in the table from an external API returning data.

================================================
FILE: examples/updates/data.go
================================================
package main
import "math/rand"
// SomeData represent some real data of some sort, unaware of tables
type SomeData struct {
ID string
Score int
Status string
}
// NewSomeData creates SomeData that has an ID and randomized values
func NewSomeData(id string) *SomeData {
s := &SomeData{
ID: id,
}
// Start with some random data
s.RandomizeScoreAndStatus()
return s
}
// RandomizeScoreAndStatus does an in-place update to simulate some data being
// updated by some other process
func (s *SomeData) RandomizeScoreAndStatus() {
s.Score = rand.Intn(100) + 1
if s.Score < 30 {
s.Status = "Critical"
} else if s.Score < 80 {
s.Status = "Stable"
} else {
s.Status = "Good"
}
}
================================================
FILE: examples/updates/main.go
================================================
package main
import (
"fmt"
"log"
"strings"
"time"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/evertras/bubble-table/table"
)
const (
columnKeyID = "id"
columnKeyScore = "score"
columnKeyStatus = "status"
)
var (
styleCritical = lipgloss.NewStyle().Foreground(lipgloss.Color("#f00"))
styleStable = lipgloss.NewStyle().Foreground(lipgloss.Color("#ff0"))
styleGood = lipgloss.NewStyle().Foreground(lipgloss.Color("#0f0"))
)
type Model struct {
table table.Model
updateDelay time.Duration
data []*SomeData
}
func rowStyleFunc(input table.RowStyleFuncInput) lipgloss.Style {
calculatedStyle := lipgloss.NewStyle()
switch input.Row.Data[columnKeyStatus] {
case "Critical":
calculatedStyle = styleCritical.Copy()
case "Stable":
calculatedStyle = styleStable.Copy()
case "Good":
calculatedStyle = styleGood.Copy()
}
if input.Index%2 == 0 {
calculatedStyle = calculatedStyle.Background(lipgloss.Color("#222"))
} else {
calculatedStyle = calculatedStyle.Background(lipgloss.Color("#444"))
}
return calculatedStyle
}
func NewModel() Model {
return Model{
table: table.New(generateColumns(0)).WithRowStyleFunc(rowStyleFunc),
updateDelay: time.Second,
}
}
// This data is stored somewhere else, maybe on a client or some other thing
func refreshDataCmd() tea.Msg {
// This could come from some API or something
return []*SomeData{
NewSomeData("abc"),
NewSomeData("def"),
NewSomeData("123"),
NewSomeData("ok"),
NewSomeData("another"),
NewSomeData("yay"),
NewSomeData("more"),
}
}
// Generate columns based on how many are critical to show some summary
func generateColumns(numCritical int) []table.Column {
// Show how many critical there are
statusStr := fmt.Sprintf("Score (%d)", numCritical)
statusCol := table.NewColumn(columnKeyStatus, statusStr, 10)
if numCritical > 3 {
// This normally applies the critical style to everything in the column,
// but in this case we apply a row style which overrides it anyway.
statusCol = statusCol.WithStyle(styleCritical)
}
return []table.Column{
table.NewColumn(columnKeyID, "ID", 10),
table.NewColumn(columnKeyScore, "Score", 8),
statusCol,
}
}
func (m Model) Init() tea.Cmd {
return refreshDataCmd
}
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var (
cmd tea.Cmd
cmds []tea.Cmd
)
m.table, cmd = m.table.Update(msg)
cmds = append(cmds, cmd)
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "esc", "q":
cmds = append(cmds, tea.Quit)
case "up":
if m.updateDelay < time.Second {
m.updateDelay *= 10
}
case "down":
if m.updateDelay > time.Millisecond*1 {
m.updateDelay /= 10
}
}
case []*SomeData:
m.data = msg
numCritical := 0
for _, d := range msg {
if d.Status == "Critical" {
numCritical++
}
}
// Reapply the new data and the new columns based on critical count
m.table = m.table.WithRows(generateRowsFromData(m.data)).WithColumns(generateColumns(numCritical))
// This can be from any source, but for demo purposes let's party!
delay := m.updateDelay
cmds = append(cmds, func() tea.Msg {
time.Sleep(delay)
return refreshDataCmd()
})
}
return m, tea.Batch(cmds...)
}
func (m Model) View() string {
body := strings.Builder{}
body.WriteString(
fmt.Sprintf(
"Table demo with updating data! Updating every %v\nPress up/down to update faster/slower\nPress q or ctrl+c to quit\n",
m.updateDelay,
))
pad := lipgloss.NewStyle().Padding(1)
body.WriteString(pad.Render(m.table.View()))
return body.String()
}
func generateRowsFromData(data []*SomeData) []table.Row {
rows := []table.Row{}
for _, entry := range data {
row := table.NewRow(table.RowData{
columnKeyID: entry.ID,
columnKeyScore: entry.Score,
columnKeyStatus: entry.Status,
})
rows = append(rows, row)
}
return rows
}
func main() {
p := tea.NewProgram(NewModel())
if err := p.Start(); err != nil {
log.Fatal(err)
}
}
================================================
FILE: flake.nix
================================================
{
inputs = {
nixpkgs.url = "nixpkgs/nixos-23.11";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
in
{
devShells.default = pkgs.mkShell {
packages = with pkgs; [
# Dev tools
go
];
};
});
}
================================================
FILE: go.mod
================================================
module github.com/evertras/bubble-table
go 1.18
require (
github.com/charmbracelet/bubbles v0.11.0
github.com/charmbracelet/bubbletea v0.21.0
github.com/charmbracelet/lipgloss v0.5.0
github.com/mattn/go-runewidth v0.0.13
github.com/muesli/reflow v0.3.0
github.com/stretchr/testify v1.7.0
)
require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/containerd/console v1.0.3 // indirect
github.com/davecgh/go-spew v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
github.com/muesli/cancelreader v0.2.0 // indirect
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
golang.org/x/sys v0.0.0-20220209214540-3681064d5158 // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
)
================================================
FILE: go.sum
================================================
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/charmbracelet/bubbles v0.11.0 h1:fBLyY0PvJnd56Vlu5L84JJH6f4axhgIJ9P3NET78f0Q=
github.com/charmbracelet/bubbles v0.11.0/go.mod h1:bbeTiXwPww4M031aGi8UK2HT9RDWoiNibae+1yCMtcc=
github.com/charmbracelet/bubbletea v0.21.0 h1:f3y+kanzgev5PA916qxmDybSHU3N804uOnKnhRPXTcI=
github.com/charmbracelet/bubbletea v0.21.0/go.mod h1:GgmJMec61d08zXsOhqRC/AiOx4K4pmz+VIcRIm1FKr4=
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DATR2/8t8=
github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs=
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34=
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
github.com/muesli/cancelreader v0.2.0 h1:SOpr+CfyVNce341kKqvbhhzQhBPyJRXQaCtn03Pae1Q=
github.com/muesli/cancelreader v0.2.0/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 h1:QANkGiGr39l1EESqrE0gZw0/AJNYzIvoGLhIoVYtluI=
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158 h1:rm+CHSpPEEW2IsXUib1ThaHIjuBVZjxNgSKmBLFfD4c=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
================================================
FILE: table/benchmarks_test.go
================================================
package table
import (
"fmt"
"testing"
)
var benchView string
func benchTable(numColumns, numDataRows int) Model {
columns := []Column{}
for i := 0; i < numColumns; i++ {
iStr := fmt.Sprintf("%d", i)
columns = append(columns, NewColumn(iStr, iStr, 6))
}
rows := []Row{}
for i := 0; i < numDataRows; i++ {
rowData := RowData{}
for columnIndex, column := range columns {
rowData[column.key] = fmt.Sprintf("%d", columnIndex)
}
rows = append(rows, NewRow(rowData))
}
return New(columns).WithRows(rows)
}
func BenchmarkPlain3x3TableView(b *testing.B) {
makeRow := func(id, name string, score int) Row {
return NewRow(RowData{
"id": id,
"name": name,
"score": score,
})
}
model := New([]Column{
NewColumn("id", "ID", 3),
NewColumn("name", "Name", 8),
NewColumn("score", "Score", 6),
}).WithRows([]Row{
makeRow("abc", "First", 17),
makeRow("def", "Second", 1034),
makeRow("123", "Third", 841),
})
b.ResetTimer()
for n := 0; n < b.N; n++ {
benchView = model.View()
}
}
func BenchmarkPlainTableViews(b *testing.B) {
sizes := []struct {
numColumns int
numRows int
}{
{
numColumns: 1,
numRows: 0,
},
{
numColumns: 10,
numRows: 0,
},
{
numColumns: 1,
numRows: 4,
},
{
numColumns: 1,
numRows: 19,
},
{
numColumns: 9,
numRows: 19,
},
{
numColumns: 9,
numRows: 49,
},
}
for _, size := range sizes {
b.Run(fmt.Sprintf("%dx%d", size.numColumns, size.numRows+1), func(b *testing.B) {
model := benchTable(size.numColumns, size.numRows)
b.ResetTimer()
for n := 0; n < b.N; n++ {
benchView = model.View()
}
})
}
}
================================================
FILE: table/border.go
================================================
package table
import "github.com/charmbracelet/lipgloss"
// Border defines the borders in and around the table.
type Border struct {
Top string
Left string
Right string
Bottom string
TopRight string
TopLeft string
BottomRight string
BottomLeft string
TopJunction string
LeftJunction string
RightJunction string
BottomJunction string
InnerJunction string
InnerDivider string
// Styles for 2x2 tables and larger
styleMultiTopLeft lipgloss.Style
styleMultiTop lipgloss.Style
styleMultiTopRight lipgloss.Style
styleMultiRight lipgloss.Style
styleMultiBottomRight lipgloss.Style
styleMultiBottom lipgloss.Style
styleMultiBottomLeft lipgloss.Style
styleMultiLeft lipgloss.Style
styleMultiInner lipgloss.Style
// Styles for a single column table
styleSingleColumnTop lipgloss.Style
styleSingleColumnInner lipgloss.Style
styleSingleColumnBottom lipgloss.Style
// Styles for a single row table
styleSingleRowLeft lipgloss.Style
styleSingleRowInner lipgloss.Style
styleSingleRowRight lipgloss.Style
// Style for a table with only one cell
styleSingleCell lipgloss.Style
// Style for the footer
styleFooter lipgloss.Style
}
var (
// https://www.w3.org/TR/xml-entity-names/025.html
borderDefault = Border{
Top: "━",
Left: "┃",
Right: "┃",
Bottom: "━",
TopRight: "┓",
TopLeft: "┏",
BottomRight: "┛",
BottomLeft: "┗",
TopJunction: "┳",
LeftJunction: "┣",
RightJunction: "┫",
BottomJunction: "┻",
InnerJunction: "╋",
InnerDivider: "┃",
}
borderRounded = Border{
Top: "─",
Left: "│",
Right: "│",
Bottom: "─",
TopRight: "╮",
TopLeft: "╭",
BottomRight: "╯",
BottomLeft: "╰",
TopJunction: "┬",
LeftJunction: "├",
RightJunction: "┤",
BottomJunction: "┴",
InnerJunction: "┼",
InnerDivider: "│",
}
)
func init() {
borderDefault.generateStyles()
borderRounded.generateStyles()
}
func (b *Border) generateStyles() {
b.generateMultiStyles()
b.generateSingleColumnStyles()
b.generateSingleRowStyles()
b.generateSingleCellStyle()
// The footer is a single cell with the top taken off... usually. We can
// re-enable the top if needed this way for certain format configurations.
b.styleFooter = b.styleSingleCell.Copy().
Align(lipgloss.Right).
BorderBottom(true).
BorderRight(true).
BorderLeft(true)
}
func (b *Border) styleLeftWithFooter(original lipgloss.Style) lipgloss.Style {
border := original.GetBorderStyle()
border.BottomLeft = b.LeftJunction
return original.Copy().BorderStyle(border)
}
func (b *Border) styleRightWithFooter(original lipgloss.Style) lipgloss.Style {
border := original.GetBorderStyle()
border.BottomRight = b.RightJunction
return original.Copy().BorderStyle(border)
}
func (b *Border) styleBothWithFooter(original lipgloss.Style) lipgloss.Style {
border := original.GetBorderStyle()
border.BottomLeft = b.LeftJunction
border.BottomRight = b.RightJunction
return original.Copy().BorderStyle(border)
}
// This function is long, but it's just repetitive...
//
//nolint:funlen
func (b *Border) generateMultiStyles() {
b.styleMultiTopLeft = lipgloss.NewStyle().BorderStyle(
lipgloss.Border{
TopLeft: b.TopLeft,
Top: b.Top,
TopRight: b.TopJunction,
Right: b.InnerDivider,
BottomRight: b.InnerJunction,
Bottom: b.Bottom,
BottomLeft: b.LeftJunction,
Left: b.Left,
},
)
b.styleMultiTop = lipgloss.NewStyle().BorderStyle(
lipgloss.Border{
Top: b.Top,
Right: b.InnerDivider,
Bottom: b.Bottom,
TopRight: b.TopJunction,
BottomRight: b.InnerJunction,
},
).BorderTop(true).BorderBottom(true).BorderRight(true)
b.styleMultiTopRight = lipgloss.NewStyle().BorderStyle(
lipgloss.Border{
Top: b.Top,
Right: b.Right,
Bottom: b.Bottom,
TopRight: b.TopRight,
BottomRight: b.RightJunction,
},
).BorderTop(true).BorderBottom(true).BorderRight(true)
b.styleMultiLeft = lipgloss.NewStyle().BorderStyle(
lipgloss.Border{
Left: b.Left,
Right: b.InnerDivider,
},
).BorderRight(true).BorderLeft(true)
b.styleMultiRight = lipgloss.NewStyle().BorderStyle(
lipgloss.Border{
Right: b.Right,
},
).BorderRight(true)
b.styleMultiInner = lipgloss.NewStyle().BorderStyle(
lipgloss.Border{
Right: b.InnerDivider,
},
).BorderRight(true)
b.styleMultiBottomLeft = lipgloss.NewStyle().BorderStyle(
lipgloss.Border{
Left: b.Left,
Right: b.InnerDivider,
Bottom: b.Bottom,
BottomLeft: b.BottomLeft,
BottomRight: b.BottomJunction,
},
).BorderLeft(true).BorderBottom(true).BorderRight(true)
b.styleMultiBottom = lipgloss.NewStyle().BorderStyle(
lipgloss.Border{
Right: b.InnerDivider,
Bottom: b.Bottom,
BottomRight: b.BottomJunction,
},
).BorderBottom(true).BorderRight(true)
b.styleMultiBottomRight = lipgloss.NewStyle().BorderStyle(
lipgloss.Border{
Right: b.Right,
Bottom: b.Bottom,
BottomRight: b.BottomRight,
},
).BorderBottom(true).BorderRight(true)
}
func (b *Border) generateSingleColumnStyles() {
b.styleSingleColumnTop = lipgloss.NewStyle().BorderStyle(
lipgloss.Border{
Top: b.Top,
Left: b.Left,
Right: b.Right,
Bottom: b.Bottom,
TopLeft: b.TopLeft,
TopRight: b.TopRight,
BottomLeft: b.LeftJunction,
BottomRight: b.RightJunction,
},
)
b.styleSingleColumnInner = lipgloss.NewStyle().BorderStyle(
lipgloss.Border{
Left: b.Left,
Right: b.Right,
},
).BorderRight(true).BorderLeft(true)
b.styleSingleColumnBottom = lipgloss.NewStyle().BorderStyle(
lipgloss.Border{
Left: b.Left,
Right: b.Right,
Bottom: b.Bottom,
BottomLeft: b.BottomLeft,
BottomRight: b.BottomRight,
},
).BorderRight(true).BorderLeft(true).BorderBottom(true)
}
func (b *Border) generateSingleRowStyles() {
b.styleSingleRowLeft = lipgloss.NewStyle().BorderStyle(
lipgloss.Border{
Top: b.Top,
Left: b.Left,
Right: b.InnerDivider,
Bottom: b.Bottom,
BottomLeft: b.BottomLeft,
BottomRight: b.BottomJunction,
TopRight: b.TopJunction,
TopLeft: b.TopLeft,
},
)
b.styleSingleRowInner = lipgloss.NewStyle().BorderStyle(
lipgloss.Border{
Top: b.Top,
Right: b.InnerDivider,
Bottom: b.Bottom,
BottomRight: b.BottomJunction,
TopRight: b.TopJunction,
},
).BorderTop(true).BorderBottom(true).BorderRight(true)
b.styleSingleRowRight = lipgloss.NewStyle().BorderStyle(
lipgloss.Border{
Top: b.Top,
Right: b.Right,
Bottom: b.Bottom,
BottomRight: b.BottomRight,
TopRight: b.TopRight,
},
).BorderTop(true).BorderBottom(true).BorderRight(true)
}
func (b *Border) generateSingleCellStyle() {
b.styleSingleCell = lipgloss.NewStyle().BorderStyle(
lipgloss.Border{
Top: b.Top,
Left: b.Left,
Right: b.Right,
Bottom: b.Bottom,
BottomLeft: b.BottomLeft,
BottomRight: b.BottomRight,
TopRight: b.TopRight,
TopLeft: b.TopLeft,
},
)
}
// BorderDefault uses the basic square border, useful to reset the border if
// it was changed somehow.
func (m Model) BorderDefault() Model {
// Already generated styles
m.border = borderDefault
return m
}
// BorderRounded uses a thin, rounded border.
func (m Model) BorderRounded() Model {
// Already generated styles
m.border = borderRounded
return m
}
// Border uses the given border components to render the table.
func (m Model) Border(border Border) Model {
border.generateStyles()
m.border = border
return m
}
type borderStyleRow struct {
left lipgloss.Style
inner lipgloss.Style
right lipgloss.Style
}
func (b *borderStyleRow) inherit(s lipgloss.Style) {
b.left = b.left.Copy().Inherit(s)
b.inner = b.inner.Copy().Inherit(s)
b.right = b.right.Copy().Inherit(s)
}
// There's a lot of branches here, but splitting it up further would make it
// harder to follow. So just be careful with comments and make sure it's tested!
//
//nolint:nestif
func (m Model) styleHeaders() borderStyleRow {
hasRows := len(m.GetVisibleRows()) > 0 || m.calculatePadding(0) > 0
singleColumn := len(m.columns) == 1
styles := borderStyleRow{}
// Possible configurations:
// - Single cell
// - Single row
// - Single column
// - Multi
if singleColumn {
if hasRows {
// Single column
styles.left = m.border.styleSingleColumnTop
styles.inner = styles.left
styles.right = styles.left
} else {
// Single cell
styles.left = m.border.styleSingleCell
styles.inner = styles.left
styles.right = styles.left
if m.hasFooter() {
styles.left = m.border.styleBothWithFooter(styles.left)
}
}
} else if !hasRows {
// Single row
styles.left = m.border.styleSingleRowLeft
styles.inner = m.border.styleSingleRowInner
styles.right = m.border.styleSingleRowRight
if m.hasFooter() {
styles.left = m.border.styleLeftWithFooter(styles.left)
styles.right = m.border.styleRightWithFooter(styles.right)
}
} else {
// Multi
styles.left = m.border.styleMultiTopLeft
styles.inner = m.border.styleMultiTop
styles.right = m.border.styleMultiTopRight
}
styles.inherit(m.headerStyle)
return styles
}
func (m Model) styleRows() (inner borderStyleRow, last borderStyleRow) {
if len(m.columns) == 1 {
inner.left = m.border.styleSingleColumnInner
inner.inner = inner.left
inner.right = inner.left
last.left = m.border.styleSingleColumnBottom
if m.hasFooter() {
last.left = m.border.styleBothWithFooter(last.left)
}
last.inner = last.left
last.right = last.left
} else {
inner.left = m.border.styleMultiLeft
inner.inner = m.border.styleMultiInner
inner.right = m.border.styleMultiRight
last.left = m.border.styleMultiBottomLeft
last.inner = m.border.styleMultiBottom
last.right = m.border.styleMultiBottomRight
if m.hasFooter() {
last.left = m.border.styleLeftWithFooter(last.left)
last.right = m.border.styleRightWithFooter(last.right)
}
}
return inner, last
}
================================================
FILE: table/calc.go
================================================
package table
// Keep compatibility with Go 1.21 by re-declaring min.
//
//nolint:predeclared
func min(x, y int) int {
if x < y {
return x
}
return y
}
// Keep compatibility with Go 1.21 by re-declaring max.
//
//nolint:predeclared
func max(x, y int) int {
if x > y {
return x
}
return y
}
// These var names are fine for this little function
//
//nolint:varnamelen
func gcd(x, y int) int {
if x == 0 {
return y
} else if y == 0 {
return x
}
return gcd(y%x, x)
}
================================================
FILE: table/calc_test.go
================================================
package table
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
// A bit overkill but let's be thorough!
func TestMin(t *testing.T) {
tests := []struct {
x int
y int
expected int
}{
{
x: 3,
y: 4,
expected: 3,
},
{
x: 3,
y: 3,
expected: 3,
},
{
x: -4,
y: 3,
expected: -4,
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%d and %d gives %d", test.x, test.y, test.expected), func(t *testing.T) {
result := min(test.x, test.y)
assert.Equal(t, test.expected, result)
})
}
}
func TestMax(t *testing.T) {
tests := []struct {
x int
y int
expected int
}{
{
x: 3,
y: 4,
expected: 4,
},
{
x: 3,
y: 3,
expected: 3,
},
{
x: -4,
y: 3,
expected: 3,
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%d and %d gives %d", test.x, test.y, test.expected), func(t *testing.T) {
result := max(test.x, test.y)
assert.Equal(t, test.expected, result)
})
}
}
func TestGCD(t *testing.T) {
tests := []struct {
x int
y int
expected int
}{
{
x: 3,
y: 4,
expected: 1,
},
{
x: 3,
y: 6,
expected: 3,
},
{
x: 4,
y: 6,
expected: 2,
},
{
x: 0,
y: 6,
expected: 6,
},
{
x: 12,
y: 0,
expected: 12,
},
{
x: 1000,
y: 100000,
expected: 1000,
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%d and %d has GCD %d", test.x, test.y, test.expected), func(t *testing.T) {
result := gcd(test.x, test.y)
assert.Equal(t, test.expected, result)
})
}
}
================================================
FILE: table/cell.go
================================================
package table
import "github.com/charmbracelet/lipgloss"
// StyledCell represents a cell in the table that has a particular style applied.
// The cell style takes highest precedence and will overwrite more general styles
// from the row, column, or table as a whole. This style should be generally
// limited to colors, font style, and alignments - spacing style such as margin
// will break the table format.
type StyledCell struct {
// Data is the content of the cell.
Data any
// Style is the specific style to apply. This is ignored if StyleFunc is not nil.
Style lipgloss.Style
// StyleFunc is a function that takes the row/column of the cell and
// returns a lipgloss.Style allowing for dynamic styling based on the cell's
// content or position. Overrides Style if set.
StyleFunc StyledCellFunc
}
// StyledCellFuncInput is the input to the StyledCellFunc. Sent as a struct
// to allow for future additions without breaking changes.
type StyledCellFuncInput struct {
// Data is the data in the cell.
Data any
// Column is the column that the cell belongs to.
Column Column
// Row is the row that the cell belongs to.
Row Row
// GlobalMetadata is the global table metadata that's been set by WithGlobalMetadata
GlobalMetadata map[string]any
}
// StyledCellFunc is a function that takes various information about the cell and
// returns a lipgloss.Style allowing for easier dynamic styling based on the cell's
// content or position.
type StyledCellFunc = func(input StyledCellFuncInput) lipgloss.Style
// NewStyledCell creates an entry that can be set in the row data and show as
// styled with the given style.
func NewStyledCell(data any, style lipgloss.Style) StyledCell {
return StyledCell{
Data: data,
Style: style,
}
}
// NewStyledCellWithStyleFunc creates an entry that can be set in the row data and show as
// styled with the given style function.
func NewStyledCellWithStyleFunc(data any, styleFunc StyledCellFunc) StyledCell {
return StyledCell{
Data: data,
StyleFunc: styleFunc,
}
}
================================================
FILE: table/column.go
================================================
package table
import (
"github.com/charmbracelet/lipgloss"
)
// Column is a column in the table.
type Column struct {
title string
key string
width int
flexFactor int
filterable bool
style lipgloss.Style
fmtString string
}
// NewColumn creates a new fixed-width column with the given information.
func NewColumn(key, title string, width int) Column {
return Column{
key: key,
title: title,
width: width,
filterable: false,
}
}
// NewFlexColumn creates a new flexible width column that tries to fill in the
// total table width. If multiple flex columns exist, each will measure against
// each other depending on their flexFactor. For example, if both have a flexFactor
// of 1, they will have equal width. If one has a flexFactor of 1 and the other
// has a flexFactor of 3, the second will be 3 times larger than the first. You
// must use WithTargetWidth if you have any flex columns, so that the table knows
// how much width it should fill.
func NewFlexColumn(key, title string, flexFactor int) Column {
return Column{
key: key,
title: title,
flexFactor: max(flexFactor, 1),
}
}
// WithStyle applies a style to the column as a whole.
func (c Column) WithStyle(style lipgloss.Style) Column {
c.style = style.Copy().Width(c.width)
return c
}
// WithFiltered sets whether the column should be considered for filtering (true)
// or not (false).
func (c Column) WithFiltered(filterable bool) Column {
c.filterable = filterable
return c
}
// WithFormatString sets the format string used by fmt.Sprintf to display the data.
// If not set, the default is "%v" for all data types. Intended mainly for
// numeric formatting.
//
// Since data is of the any type, make sure that all data in the column
// is of the expected type or the format may fail. For example, hardcoding '3'
// instead of '3.0' and using '%.2f' will fail because '3' is an integer.
func (c Column) WithFormatString(fmtString string) Column {
c.fmtString = fmtString
return c
}
func (c *Column) isFlex() bool {
return c.flexFactor != 0
}
// Title returns the title of the column.
func (c Column) Title() string {
return c.title
}
// Key returns the key of the column.
func (c Column) Key() string {
return c.key
}
// Width returns the width of the column.
func (c Column) Width() int {
return c.width
}
// FlexFactor returns the flex factor of the column.
func (c Column) FlexFactor() int {
return c.flexFactor
}
// IsFlex returns whether the column is a flex column.
func (c Column) IsFlex() bool {
return c.isFlex()
}
// Filterable returns whether the column is filterable.
func (c Column) Filterable() bool {
return c.filterable
}
// Style returns the style of the column.
func (c Column) Style() lipgloss.Style {
return c.style
}
// FmtString returns the format string of the column.
func (c Column) FmtString() string {
return c.fmtString
}
================================================
FILE: table/column_test.go
================================================
package table
import (
"fmt"
"testing"
"github.com/charmbracelet/lipgloss"
"github.com/stretchr/testify/assert"
)
func TestColumnTitle(t *testing.T) {
tests := []struct {
title string
expected string
}{
{
title: "foo",
expected: "foo",
},
{
title: "bar",
expected: "bar",
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("title %s gives %s", test.title, test.expected), func(t *testing.T) {
col := NewColumn("key", test.title, 10)
assert.Equal(t, test.expected, col.Title())
})
}
}
func TestColumnKey(t *testing.T) {
tests := []struct {
key string
expected string
}{
{
key: "foo",
expected: "foo",
},
{
key: "bar",
expected: "bar",
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("key %s gives %s", test.key, test.expected), func(t *testing.T) {
col := NewColumn(test.key, "title", 10)
assert.Equal(t, test.expected, col.Key())
})
}
}
func TestColumnWidth(t *testing.T) {
tests := []struct {
width int
expected int
}{
{
width: 3,
expected: 3,
},
{
width: 4,
expected: 4,
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("width %d gives %d", test.width, test.expected), func(t *testing.T) {
col := NewColumn("key", "title", test.width)
assert.Equal(t, test.expected, col.Width())
})
}
}
func TestColumnFlexFactor(t *testing.T) {
tests := []struct {
flexFactor int
expected int
}{
{
flexFactor: 3,
expected: 3,
},
{
flexFactor: 4,
expected: 4,
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("flexFactor %d gives %d", test.flexFactor, test.expected), func(t *testing.T) {
col := NewFlexColumn("key", "title", test.flexFactor)
assert.Equal(t, test.expected, col.FlexFactor())
})
}
}
func TestColumnIsFlex(t *testing.T) {
testsFlexColumn := []struct {
flexFactor int
expected bool
}{
{
flexFactor: 3,
expected: true,
},
{
flexFactor: 0,
expected: true,
},
}
for _, test := range testsFlexColumn {
t.Run(fmt.Sprintf("flexFactor %d gives %t", test.flexFactor, test.expected), func(t *testing.T) {
col := NewFlexColumn("key", "title", test.flexFactor)
assert.Equal(t, test.expected, col.IsFlex())
})
}
testsRegularColumn := []struct {
width int
expected bool
}{
{
width: 3,
expected: false,
},
{
width: 0,
expected: false,
},
}
for _, test := range testsRegularColumn {
t.Run(fmt.Sprintf("width %d gives %t", test.width, test.expected), func(t *testing.T) {
col := NewColumn("key", "title", test.width)
assert.Equal(t, test.expected, col.IsFlex())
})
}
}
func TestColumnFilterable(t *testing.T) {
tests := []struct {
filterable bool
expected bool
}{
{
filterable: true,
expected: true,
},
{
filterable: false,
expected: false,
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("filterable %t gives %t", test.filterable, test.expected), func(t *testing.T) {
col := NewColumn("key", "title", 10)
col = col.WithFiltered(test.filterable)
assert.Equal(t, test.expected, col.Filterable())
})
}
}
func TestColumnStyle(t *testing.T) {
width := 10
tests := []struct {
style lipgloss.Style
expected lipgloss.Style
}{
{
style: lipgloss.NewStyle(),
expected: lipgloss.NewStyle().Width(width),
},
{
style: lipgloss.NewStyle().Bold(true),
expected: lipgloss.NewStyle().Bold(true).Width(width),
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("style %v gives %v", test.style, test.expected), func(t *testing.T) {
col := NewColumn("key", "title", width).WithStyle(test.style)
assert.Equal(t, test.expected, col.Style())
})
}
}
func TestColumnFormatString(t *testing.T) {
tests := []struct {
fmtString string
expected string
}{
{
fmtString: "%v",
expected: "%v",
},
{
fmtString: "%.2f",
expected: "%.2f",
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("fmtString %s gives %s", test.fmtString, test.expected), func(t *testing.T) {
col := NewColumn("key", "title", 10)
col = col.WithFormatString(test.fmtString)
assert.Equal(t, test.expected, col.FmtString())
})
}
}
================================================
FILE: table/data.go
================================================
package table
import "time"
// This is just a bunch of data type checks, so... no linting here
//
//nolint:cyclop
func asInt(data any) (int64, bool) {
switch val := data.(type) {
case int:
return int64(val), true
case int8:
return int64(val), true
case int16:
return int64(val), true
case int32:
return int64(val), true
case int64:
return val, true
case uint:
// #nosec: G115
return int64(val), true
case uint8:
return int64(val), true
case uint16:
return int64(val), true
case uint32:
return int64(val), true
case uint64:
// #nosec: G115
return int64(val), true
case time.Duration:
return int64(val), true
case StyledCell:
return asInt(val.Data)
}
return 0, false
}
func asNumber(data any) (float64, bool) {
switch val := data.(type) {
case float32:
return float64(val), true
case float64:
return val, true
case StyledCell:
return asNumber(val.Data)
}
intVal, isInt := asInt(data)
return float64(intVal), isInt
}
================================================
FILE: table/data_test.go
================================================
package table
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestAsInt(t *testing.T) {
check := func(data any, isInt bool, expectedValue int64) {
val, ok := asInt(data)
assert.Equal(t, isInt, ok)
assert.Equal(t, expectedValue, val)
}
check(3, true, 3)
check(3.3, false, 0)
check(int8(3), true, 3)
check(int16(3), true, 3)
check(int32(3), true, 3)
check(int64(3), true, 3)
check(uint(3), true, 3)
check(uint8(3), true, 3)
check(uint16(3), true, 3)
check(uint32(3), true, 3)
check(uint64(3), true, 3)
check(StyledCell{Data: 3}, true, 3)
check(time.Duration(3), true, 3)
}
func TestAsNumber(t *testing.T) {
check := func(data any, isFloat bool, expectedValue float64) {
val, ok := asNumber(data)
assert.Equal(t, isFloat, ok)
assert.InDelta(t, expectedValue, val, 0.001)
}
check(uint32(3), true, 3)
check(3.3, true, 3.3)
check(float32(3.3), true, 3.3)
check(StyledCell{Data: 3.3}, true, 3.3)
}
================================================
FILE: table/dimensions.go
================================================
package table
import (
"github.com/charmbracelet/lipgloss"
)
func (m *Model) recalculateWidth() {
if m.targetTotalWidth != 0 {
m.totalWidth = m.targetTotalWidth
} else {
total := 0
for _, column := range m.columns {
total += column.width
}
m.totalWidth = total + len(m.columns) + 1
}
updateColumnWidths(m.columns, m.targetTotalWidth)
m.recalculateLastHorizontalColumn()
}
// Updates column width in-place. This could be optimized but should be called
// very rarely so we prioritize simplicity over performance here.
func updateColumnWidths(cols []Column, totalWidth int) {
totalFlexWidth := totalWidth - len(cols) - 1
totalFlexFactor := 0
flexGCD := 0
for index, col := range cols {
if !col.isFlex() {
totalFlexWidth -= col.width
cols[index].style = col.style.Width(col.width)
} else {
totalFlexFactor += col.flexFactor
flexGCD = gcd(flexGCD, col.flexFactor)
}
}
if totalFlexFactor == 0 {
return
}
// We use the GCD here because otherwise very large values won't divide
// nicely as ints
totalFlexFactor /= flexGCD
flexUnit := totalFlexWidth / totalFlexFactor
leftoverWidth := totalFlexWidth % totalFlexFactor
for index := range cols {
if !cols[index].isFlex() {
continue
}
width := flexUnit * (cols[index].flexFactor / flexGCD)
if leftoverWidth > 0 {
width++
leftoverWidth--
}
if index == len(cols)-1 {
width += leftoverWidth
leftoverWidth = 0
}
width = max(width, 1)
cols[index].width = width
// Take borders into account for the actual style
cols[index].style = cols[index].style.Width(width)
}
}
func (m *Model) recalculateHeight() {
header := m.renderHeaders()
headerHeight := 1 // Header always has the top border
if m.headerVisible {
headerHeight = lipgloss.Height(header)
}
footer := m.renderFooter(lipgloss.Width(header), false)
var footerHeight int
if footer != "" {
footerHeight = lipgloss.Height(footer)
}
m.metaHeight = headerHeight + footerHeight
}
func (m *Model) calculatePadding(numRows int) int {
if m.minimumHeight == 0 {
return 0
}
padding := m.minimumHeight - m.metaHeight - numRows - 1 // additional 1 for bottom border
if padding == 0 && numRows == 0 {
// This is an edge case where we want to add 1 additional line of height, i.e.
// add a border without an empty row. However, this is not possible, so we need
// to add an extra row which will result in the table being 1 row taller than
// the requested minimum height.
return 1
}
if padding < 0 {
// Table is already larger than minimum height, do nothing.
return 0
}
return padding
}
================================================
FILE: table/dimensions_test.go
================================================
package table
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
// This function is only long because of repetitive test definitions, this is fine
//
//nolint:funlen
func TestColumnUpdateWidths(t *testing.T) {
tests := []struct {
name string
columns []Column
totalWidth int
expectedWidths []int
}{
{
name: "Static",
columns: []Column{
NewColumn("abc", "a", 4),
NewColumn("sdf", "b", 7),
NewColumn("xyz", "c", 2),
},
totalWidth: 13,
expectedWidths: []int{
4, 7, 2,
},
},
{
name: "Even half",
columns: []Column{
NewFlexColumn("abc", "a", 1),
NewFlexColumn("sdf", "b", 1),
},
totalWidth: 11,
expectedWidths: []int{
4, 4,
},
},
{
name: "Odd half increases first",
columns: []Column{
NewFlexColumn("abc", "a", 1),
NewFlexColumn("sdf", "b", 1),
},
totalWidth: 12,
expectedWidths: []int{
5, 4,
},
},
{
name: "Even fourths",
columns: []Column{
NewFlexColumn("abc", "a", 1),
NewFlexColumn("sdf", "b", 1),
NewFlexColumn("xyz", "c", 1),
NewFlexColumn("123", "d", 1),
},
totalWidth: 17,
expectedWidths: []int{
3, 3, 3, 3,
},
},
{
name: "Odd fourths",
columns: []Column{
NewFlexColumn("abc", "a", 1),
NewFlexColumn("sdf", "b", 1),
NewFlexColumn("xyz", "c", 1),
NewFlexColumn("123", "d", 1),
},
totalWidth: 20,
expectedWidths: []int{
4, 4, 4, 3,
},
},
{
name: "Simple mix static and flex",
columns: []Column{
NewColumn("abc", "a", 5),
NewFlexColumn("flex", "flex", 1),
},
totalWidth: 18,
expectedWidths: []int{
5, 10,
},
},
{
name: "Static and flex with high flex factor",
columns: []Column{
NewColumn("abc", "a", 5),
NewFlexColumn("flex", "flex", 1000),
},
totalWidth: 18,
expectedWidths: []int{
5, 10,
},
},
{
name: "Static and multiple flexes with high flex factor",
columns: []Column{
NewColumn("abc", "a", 5),
NewFlexColumn("flex", "flex", 1000),
NewFlexColumn("flex", "flex", 1000),
NewFlexColumn("flex", "flex", 1000),
},
totalWidth: 22,
expectedWidths: []int{
5, 4, 4, 4,
},
},
{
name: "Static and multiple flexes of different sizes",
columns: []Column{
NewFlexColumn("flex", "flex", 1),
NewColumn("abc", "a", 5),
NewFlexColumn("flex", "flex", 2),
NewFlexColumn("flex", "flex", 1),
},
totalWidth: 22,
expectedWidths: []int{
3, 5, 6, 3,
},
},
{
name: "Width is too small",
columns: []Column{
NewColumn("abc", "a", 5),
NewFlexColumn("flex", "flex", 2),
NewFlexColumn("flex", "flex", 1),
},
totalWidth: 3,
expectedWidths: []int{
5, 1, 1,
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
updateColumnWidths(test.columns, test.totalWidth)
for i, col := range test.columns {
assert.Equal(t, test.expectedWidths[i], col.width, fmt.Sprintf("index %d", i))
}
})
}
}
// This function is long because it has many test cases
//
//nolint:funlen
func TestRecalculateHeight(t *testing.T) {
columns := []Column{
NewColumn("ka", "a", 3),
NewColumn("kb", "b", 4),
NewColumn("kc", "c", 5),
}
rows := []Row{
NewRow(RowData{"ka": 1, "kb": 23, "kc": "zyx"}),
NewRow(RowData{"ka": 3, "kb": 34, "kc": "wvu"}),
NewRow(RowData{"ka": 5, "kb": 45, "kc": "zyx"}),
NewRow(RowData{"ka": 7, "kb": 56, "kc": "wvu"}),
}
tests := []struct {
name string
model Model
expectedHeight int
}{
{
name: "Default header",
model: New(columns).WithRows(rows),
expectedHeight: 3,
},
{
name: "Empty page with default header",
model: New(columns),
expectedHeight: 3,
},
{
name: "Filtered with default header",
model: New(columns).WithRows(rows).Filtered(true),
expectedHeight: 5,
},
{
name: "Static footer one line",
model: New(columns).WithRows(rows).WithStaticFooter("single line"),
expectedHeight: 5,
},
{
name: "Static footer overflow",
model: New(columns).WithRows(rows).
WithStaticFooter("single line but it's long"),
expectedHeight: 6,
},
{
name: "Static footer multi-line",
model: New(columns).WithRows(rows).
WithStaticFooter("footer with\nmultiple lines"),
expectedHeight: 6,
},
{
name: "Paginated",
model: New(columns).WithRows(rows).WithPageSize(2),
expectedHeight: 5,
},
{
name: "No pagination",
model: New(columns).WithRows(rows).WithPageSize(2).WithNoPagination(),
expectedHeight: 3,
},
{
name: "Footer not visible",
model: New(columns).WithRows(rows).Filtered(true).WithFooterVisibility(false),
expectedHeight: 3,
},
{
name: "Header not visible",
model: New(columns).WithRows(rows).WithHeaderVisibility(false),
expectedHeight: 1,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
test.model.recalculateHeight()
assert.Equal(t, test.expectedHeight, test.model.metaHeight)
})
}
}
================================================
FILE: table/doc.go
================================================
/*
Package table contains a Bubble Tea component for an interactive and customizable
table.
The simplest useful table can be created with table.New(...).WithRows(...). Row
data should map to the column keys, as shown below. Note that extra data will
simply not be shown, while missing data will be safely blank in the row's cell.
const (
// This is not necessary, but recommended to avoid typos
columnKeyName = "name"
columnKeyCount = "count"
)
// Define the columns and how they appear
columns := []table.Column{
table.NewColumn(columnKeyName, "Name", 10),
table.NewColumn(columnKeyCount, "Count", 6),
}
// Define the data that will be in the table, mapping to the column keys
rows := []table.Row{
table.NewRow(table.RowData{
columnKeyName: "Cheeseburger",
columnKeyCount: 3,
}),
table.NewRow(table.RowData{
columnKeyName: "Fries",
columnKeyCount: 2,
}),
}
// Create the table
tbl := table.New(columns).WithRows(rows)
// Use it like any Bubble Tea component in your view
tbl.View()
*/
package table
================================================
FILE: table/events.go
================================================
package table
// UserEvent is some state change that has occurred due to user input. These will
// ONLY be generated when a user has interacted directly with the table. These
// will NOT be generated when code programmatically changes values in the table.
type UserEvent any
func (m *Model) appendUserEvent(e UserEvent) {
m.lastUpdateUserEvents = append(m.lastUpdateUserEvents, e)
}
func (m *Model) clearUserEvents() {
m.lastUpdateUserEvents = nil
}
// GetLastUpdateUserEvents returns a list of events that happened due to user
// input in the last Update call. This is useful to look for triggers such as
// whether the user moved to a new highlighted row.
func (m *Model) GetLastUpdateUserEvents() []UserEvent {
// Most common case
if len(m.lastUpdateUserEvents) == 0 {
return nil
}
returned := make([]UserEvent, len(m.lastUpdateUserEvents))
// Slightly wasteful but helps guarantee immutability, and this should only
// have data very rarely so this is fine
copy(returned, m.lastUpdateUserEvents)
return returned
}
// UserEventHighlightedIndexChanged indicates that the user has scrolled to a new
// row.
type UserEventHighlightedIndexChanged struct {
// PreviousRow is the row that was selected before the change.
PreviousRowIndex int
// SelectedRow is the row index that is now selected
SelectedRowIndex int
}
// UserEventRowSelectToggled indicates that the user has either selected or
// deselected a row by toggling the selection. The event contains information
// about which row index was selected and whether it was selected or deselected.
type UserEventRowSelectToggled struct {
RowIndex int
IsSelected bool
}
// UserEventFilterInputFocused indicates that the user has focused the filter
// text input, so that any other typing will type into the filter field. Only
// activates for the built-in filter text box.
type UserEventFilterInputFocused struct{}
// UserEventFilterInputUnfocused indicates that the user has unfocused the filter
// text input, which means the user is done typing into the filter field. Only
// activates for the built-in filter text box.
type UserEventFilterInputUnfocused struct{}
================================================
FILE: table/events_test.go
================================================
package table
import (
"testing"
tea "github.com/charmbracelet/bubbletea"
"github.com/stretchr/testify/assert"
)
func TestUserEventsEmptyWhenNothingHappens(t *testing.T) {
model := New([]Column{})
events := model.GetLastUpdateUserEvents()
assert.Len(t, events, 0, "Should be empty when nothing has happened")
model, _ = model.Update(nil)
events = model.GetLastUpdateUserEvents()
assert.Len(t, events, 0, "Should be empty when no changes made in Update")
}
//nolint:funlen // This is a bunch of checks in a row, this is fine
func TestUserEventHighlightedIndexChanged(t *testing.T) {
// Don't need any actual row data for this
empty := RowData{}
model := New([]Column{}).
Focused(true).
WithRows(
[]Row{
NewRow(empty),
NewRow(empty),
NewRow(empty),
NewRow(empty),
},
)
hitDown := func() {
model, _ = model.Update(tea.KeyMsg{Type: tea.KeyDown})
}
hitUp := func() {
model, _ = model.Update(tea.KeyMsg{Type: tea.KeyUp})
}
checkEvent := func(events []UserEvent, expectedPreviousIndex, expectedCurrentIndex int) {
if len(events) != 1 {
assert.FailNow(t, "Asked to check events with len of not 1, test is bad")
}
switch event := events[0].(type) {
case UserEventHighlightedIndexChanged:
assert.Equal(t, expectedPreviousIndex, event.PreviousRowIndex)
assert.Equal(t, expectedCurrentIndex, event.SelectedRowIndex)
default:
assert.Failf(t, "Event is not expected type UserEventHighlightedIndexChanged", "%+v", event)
}
}
events := model.GetLastUpdateUserEvents()
assert.Len(t, events, 0, "Should be empty when nothing has happened")
// Hit down to change row down by one
hitDown()
events = model.GetLastUpdateUserEvents()
assert.Len(t, events, 1, "Missing event for scrolling down")
checkEvent(events, 0, 1)
// Do some no-op
model, _ = model.Update(nil)
events = model.GetLastUpdateUserEvents()
assert.Len(t, events, 0, "Events not cleared between Updates")
// Hit up to go back to top
hitUp()
events = model.GetLastUpdateUserEvents()
assert.Len(t, events, 1, "Missing event to scroll back up")
checkEvent(events, 1, 0)
// Hit up to scroll around to bottom
hitUp()
events = model.GetLastUpdateUserEvents()
assert.Len(t, events, 1, "Missing event to scroll up with wrap")
checkEvent(events, 0, 3)
// Now check to make sure it doesn't trigger when row index doesn't change
model = model.WithRows([]Row{NewRow(empty)})
hitDown()
events = model.GetLastUpdateUserEvents()
assert.Len(t, events, 0, "There's no row to change to for single row table, event shouldn't exist")
model = model.WithRows([]Row{})
hitDown()
events = model.GetLastUpdateUserEvents()
assert.Len(t, events, 0, "There's no row to change to for an empty table, event shouldn't exist")
}
//nolint:funlen // This is a bunch of checks in a row, this is fine
func TestUserEventRowSelectToggled(t *testing.T) {
// Don't need any actual row data for this
empty := RowData{}
model := New([]Column{}).
Focused(true).
WithRows(
[]Row{
NewRow(empty),
NewRow(empty),
NewRow(empty),
NewRow(empty),
},
).
SelectableRows(true)
hitDown := func() {
model, _ = model.Update(tea.KeyMsg{Type: tea.KeyDown})
}
hitSelectToggle := func() {
model, _ = model.Update(tea.KeyMsg{Type: tea.KeySpace})
}
checkEvent := func(events []UserEvent, expectedRowIndex int, expectedSelectionState bool) {
if len(events) != 1 {
assert.FailNow(t, "Asked to check events with len of not 1, test is bad")
}
switch event := events[0].(type) {
case UserEventRowSelectToggled:
assert.Equal(t, expectedRowIndex, event.RowIndex, "Row index wrong")
assert.Equal(t, expectedSelectionState, event.IsSelected, "Selection state wrong")
default:
assert.Failf(t, "Event is not expected type UserEventRowSelectToggled", "%+v", event)
}
}
events := model.GetLastUpdateUserEvents()
assert.Len(t, events, 0, "Should be empty when nothing has happened")
// Try initial selection
hitSelectToggle()
events = model.GetLastUpdateUserEvents()
assert.Len(t, events, 1, "Missing event for selection toggle")
checkEvent(events, 0, true)
// Do some no-op
model, _ = model.Update(nil)
events = model.GetLastUpdateUserEvents()
assert.Len(t, events, 0, "Events not cleared between Updates")
// Check deselection
hitSelectToggle()
events = model.GetLastUpdateUserEvents()
assert.Len(t, events, 1, "Missing event to toggle select for second time")
checkEvent(events, 0, false)
// Try one row down... note that the row change event should clear after the
// first keypress
hitDown()
hitSelectToggle()
events = model.GetLastUpdateUserEvents()
assert.Len(t, events, 1, "Missing event after scrolling down")
checkEvent(events, 1, true)
// Check edge case of empty table
model = model.WithRows([]Row{})
hitSelectToggle()
events = model.GetLastUpdateUserEvents()
assert.Len(t, events, 0, "There's no row to select for an empty table, event shouldn't exist")
}
func TestFilterFocusEvents(t *testing.T) {
model := New([]Column{}).Filtered(true).Focused(true)
events := model.GetLastUpdateUserEvents()
assert.Empty(t, events, "Unexpected events to start")
// Start filter
model, _ = model.Update(tea.KeyMsg{
Type: tea.KeyRunes,
Runes: []rune{'/'},
})
events = model.GetLastUpdateUserEvents()
assert.Len(t, events, 1, "Only expected one event")
switch events[0].(type) {
case UserEventFilterInputFocused:
default:
assert.FailNow(t, "Unexpected event type")
}
// Stop filter
model, _ = model.Update(tea.KeyMsg{
Type: tea.KeyEnter,
})
events = model.GetLastUpdateUserEvents()
assert.Len(t, events, 1, "Only expected one event")
switch events[0].(type) {
case UserEventFilterInputUnfocused:
default:
assert.FailNow(t, "Unexpected event type")
}
}
================================================
FILE: table/filter.go
================================================
package table
import (
"fmt"
"strings"
)
// FilterFuncInput is the input to a FilterFunc. It's a struct so we can add more things later
// without breaking compatibility.
type FilterFuncInput struct {
// Columns is a list of the columns of the table
Columns []Column
// Row is the row that's being considered for filtering
Row Row
// GlobalMetadata is an arbitrary set of metadata from the table set by WithGlobalMetadata
GlobalMetadata map[string]any
// Filter is the filter string input to consider
Filter string
}
// FilterFunc takes a FilterFuncInput and returns true if the row should be visible,
// or false if the row should be hidden.
type FilterFunc func(FilterFuncInput) bool
func (m Model) getFilteredRows(rows []Row) []Row {
filterInputValue := m.filterTextInput.Value()
if !m.filtered || filterInputValue == "" {
return rows
}
filteredRows := make([]Row, 0)
for _, row := range rows {
var availableFilterFunc FilterFunc
if m.filterFunc != nil {
availableFilterFunc = m.filterFunc
} else {
availableFilterFunc = filterFuncContains
}
if availableFilterFunc(FilterFuncInput{
Columns: m.columns,
Row: row,
Filter: filterInputValue,
GlobalMetadata: m.metadata,
}) {
filteredRows = append(filteredRows, row)
}
}
return filteredRows
}
// filterFuncContains returns a filterFunc that performs case-insensitive
// "contains" matching over all filterable columns in a row.
func filterFuncContains(input FilterFuncInput) bool {
if input.Filter == "" {
return true
}
checkedAny := false
filterLower := strings.ToLower(input.Filter)
for _, column := range input.Columns {
if !column.filterable {
continue
}
checkedAny = true
data, ok := input.Row.Data[column.key]
if !ok {
continue
}
// Extract internal StyledCell data
switch dataV := data.(type) {
case StyledCell:
data = dataV.Data
}
var target string
switch dataV := data.(type) {
case string:
target = dataV
case fmt.Stringer:
target = dataV.String()
default:
target = fmt.Sprintf("%v", data)
}
if strings.Contains(strings.ToLower(target), filterLower) {
return true
}
}
return !checkedAny
}
// filterFuncFuzzy returns a filterFunc that performs case-insensitive fuzzy
// matching (subsequence) over the concatenation of all filterable column values.
func filterFuncFuzzy(input FilterFuncInput) bool {
filter := strings.TrimSpace(input.Filter)
if filter == "" {
return true
}
var builder strings.Builder
for _, col := range input.Columns {
if !col.filterable {
continue
}
value, ok := input.Row.Data[col.key]
if !ok {
continue
}
if sc, ok := value.(StyledCell); ok {
value = sc.Data
}
builder.WriteString(fmt.Sprint(value)) // uses Stringer if implemented
builder.WriteByte(' ')
}
haystack := strings.ToLower(builder.String())
if haystack == "" {
return false
}
for _, token := range strings.Fields(strings.ToLower(filter)) {
if !fuzzySubsequenceMatch(haystack, token) {
return false
}
}
return true
}
// fuzzySubsequenceMatch returns true if all runes in needle appear in order
// within haystack (not necessarily contiguously). Case must be normalized by caller.
func fuzzySubsequenceMatch(haystack, needle string) bool {
if needle == "" {
return true
}
haystackIndex, needleIndex := 0, 0
haystackRunes := []rune(haystack)
needleRunes := []rune(needle)
for haystackIndex < len(haystackRunes) && needleIndex < len(needleRunes) {
if haystackRunes[haystackIndex] == needleRunes[needleIndex] {
needleIndex++
}
haystackIndex++
}
return needleIndex == len(needleRunes)
}
================================================
FILE: table/filter_test.go
================================================
package table
import (
"fmt"
"testing"
"time"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/stretchr/testify/assert"
)
func TestIsRowMatched(t *testing.T) {
columns := []Column{
NewColumn("title", "title", 10).WithFiltered(true),
NewColumn("description", "description", 10)}
assert.True(t, filterFuncContains(FilterFuncInput{
Columns: columns,
Row: NewRow(RowData{
"title": "AAA",
"description": "",
}),
Filter: "",
}))
type testCase struct {
name string
filter string
title any
description any
shouldMatch bool
}
timeFrom2020 := time.Date(2020, time.July, 1, 1, 1, 1, 1, time.UTC)
cases := []testCase{
{"empty filter matches all", "", "AAA", "", true},
{"exact match", "AAA", "AAA", "", true},
{"partial match start", "A", "AAA", "", true},
{"partial match middle", "AA", "AAA", "", true},
{"too long", "AAAA", "AAA", "", false},
{"lowercase", "aaa", "AAA", "", true},
{"mixed case", "AaA", "AAA", "", true},
{"wrong input", "B", "AAA", "", false},
{"ignore description", "BBB", "AAA", "BBB", false},
{"time filterable success", "2020", timeFrom2020, "", true},
{"time filterable wrong input", "2021", timeFrom2020, "", false},
{"styled cell", "AAA", NewStyledCell("AAA", lipgloss.NewStyle()), "", true},
}
for _, testCase := range cases {
t.Run(testCase.name, func(t *testing.T) {
assert.Equal(t, testCase.shouldMatch, filterFuncContains(FilterFuncInput{
Columns: columns,
Row: NewRow(RowData{
"title": testCase.title,
"description": testCase.description,
}),
Filter: testCase.filter,
}))
})
}
// Styled check
}
func TestIsRowMatchedForNonStringer(t *testing.T) {
columns := []Column{
NewColumn("val", "val", 10).WithFiltered(true),
}
type testCase struct {
name string
filter string
val any
shouldMatch bool
}
cases := []testCase{
{"exact match", "12", 12, true},
{"partial match", "1", 12, true},
{"partial match end", "2", 12, true},
{"wrong input", "3", 12, false},
}
for _, testCase := range cases {
t.Run(testCase.name, func(t *testing.T) {
assert.Equal(t, testCase.shouldMatch, filterFuncContains(FilterFuncInput{
Columns: columns,
Row: NewRow(RowData{
"val": testCase.val,
}),
Filter: testCase.filter,
}))
})
}
}
func TestGetFilteredRowsNoColumnFiltered(t *testing.T) {
columns := []Column{NewColumn("title", "title", 10)}
rows := []Row{
NewRow(RowData{
"title": "AAA",
"description": "",
}),
NewRow(RowData{
"title": "BBB",
"description": "",
}),
NewRow(RowData{
"title": "CCC",
"description": "",
}),
}
model := New(columns).WithRows(rows).Filtered(true)
model.filterTextInput.SetValue("AAA")
filteredRows := model.getFilteredRows(rows)
assert.Len(t, filteredRows, len(rows))
}
func TestGetFilteredRowsUnfiltered(t *testing.T) {
columns := []Column{NewColumn("title", "title", 10)}
rows := []Row{
NewRow(RowData{
"title": "AAA",
}),
NewRow(RowData{
"title": "BBB",
}),
}
model := New(columns).WithRows(rows)
filteredRows := model.getFilteredRows(rows)
assert.Len(t, filteredRows, len(rows))
}
func TestGetFilteredRowsFiltered(t *testing.T) {
columns := []Column{NewColumn("title", "title", 10).WithFiltered(true)}
rows := []Row{
NewRow(RowData{
"title": "AAA",
"description": "",
}),
NewRow(RowData{
"title": "BBB",
"description": "",
}),
// Empty
NewRow(RowData{}),
}
model := New(columns).WithRows(rows).Filtered(true)
model.filterTextInput.SetValue("AaA")
filteredRows := model.getFilteredRows(rows)
assert.Len(t, filteredRows, 1)
}
func TestGetFilteredRowsRefocusAfterFilter(t *testing.T) {
columns := []Column{NewColumn("title", "title", 10).WithFiltered(true)}
rows := []Row{
NewRow(RowData{
"title": "a",
"description": "",
}),
NewRow(RowData{
"title": "b",
"description": "",
}),
NewRow(RowData{
"title": "c",
"description": "",
}),
NewRow(RowData{
"title": "d1",
"description": "",
}),
NewRow(RowData{
"title": "d2",
"description": "",
}),
}
model := New(columns).WithRows(rows).Filtered(true).WithPageSize(1)
model = model.PageDown()
assert.Len(t, model.GetVisibleRows(), 5)
assert.Equal(t, 1, model.PageSize())
assert.Equal(t, 2, model.CurrentPage())
assert.Equal(t, 5, model.MaxPages())
assert.Equal(t, 5, model.TotalRows())
model.filterTextInput.SetValue("c")
model, _ = model.updateFilterTextInput(tea.KeyMsg{})
assert.Len(t, model.GetVisibleRows(), 1)
assert.Equal(t, 1, model.PageSize())
assert.Equal(t, 1, model.CurrentPage())
assert.Equal(t, 1, model.MaxPages())
assert.Equal(t, 1, model.TotalRows())
model.filterTextInput.SetValue("not-exist")
model, _ = model.updateFilterTextInput(tea.KeyMsg{})
assert.Len(t, model.GetVisibleRows(), 0)
assert.Equal(t, 1, model.PageSize())
assert.Equal(t, 1, model.CurrentPage())
assert.Equal(t, 1, model.MaxPages())
assert.Equal(t, 0, model.TotalRows())
}
func TestFilterWithExternalTextInput(t *testing.T) {
columns := []Column{NewColumn("title", "title", 10).WithFiltered(true)}
rows := []Row{
NewRow(RowData{
"title": "AAA",
"description": "",
}),
NewRow(RowData{
"title": "BBB",
"description": "",
}),
// Empty
NewRow(RowData{}),
}
// Page size 1 to test scrolling back if input changes
model := New(columns).WithRows(rows).Filtered(true).WithPageSize(1)
model.pageDown()
assert.Equal(t, 2, model.CurrentPage(), "Should start on second page for test")
input := textinput.New()
input.SetValue("AaA")
model = model.WithFilterInput(input)
assert.Equal(t, 1, model.CurrentPage(), "Did not go back to first page")
filteredRows := model.getFilteredRows(rows)
assert.Len(t, filteredRows, 1)
}
func TestFilterWithSetValue(t *testing.T) {
columns := []Column{NewColumn("title", "title", 10).WithFiltered(true)}
rows := []Row{
NewRow(RowData{
"title": "AAA",
"description": "",
}),
NewRow(RowData{
"title": "BBB",
"description": "",
}),
// Empty
NewRow(RowData{}),
}
// Page size 1 to make sure we scroll back correctly
model := New(columns).WithRows(rows).Filtered(true).WithPageSize(1)
model.pageDown()
assert.Equal(t, 2, model.CurrentPage(), "Should start on second page for test")
model = model.WithFilterInputValue("AaA")
assert.Equal(t, 1, model.CurrentPage(), "Did not go back to first page")
filteredRows := model.getFilteredRows(rows)
assert.Len(t, filteredRows, 1)
// Make sure it holds true after an update
model, _ = model.Update(tea.KeyRight)
filteredRows = model.getFilteredRows(rows)
assert.Len(t, filteredRows, 1)
// Remove filter
model = model.WithFilterInputValue("")
filteredRows = model.getFilteredRows(rows)
assert.Len(t, filteredRows, 3)
}
func TestFilterFunc(t *testing.T) {
const (
colTitle = "title"
colDesc = "description"
)
columns := []Column{NewColumn("title", "title", 10).WithFiltered(true)}
rows := []Row{
NewRow(RowData{
colTitle: "AAA",
colDesc: "",
}),
NewRow(RowData{
colTitle: "BBB",
colDesc: "",
}),
// Empty
NewRow(RowData{}),
}
filterFunc := func(input FilterFuncInput) bool {
// Completely arbitrary check for testing purposes
title := fmt.Sprintf("%v", input.Row.Data["title"])
return title == "AAA" && input.Filter == "x" && input.GlobalMetadata["testValue"] == 3
}
// First check that the table won't match with different case
model := New(columns).WithRows(rows).Filtered(true).WithGlobalMetadata(map[string]any{
"testValue": 3,
})
model = model.WithFilterInputValue("x")
filteredRows := model.getFilteredRows(rows)
assert.Len(t, filteredRows, 0)
// The filter func should then match the one row
model = model.WithFilterFunc(filterFunc)
filteredRows = model.getFilteredRows(rows)
assert.Len(t, filteredRows, 1)
// Remove filter
model = model.WithFilterInputValue("")
filteredRows = model.getFilteredRows(rows)
assert.Len(t, filteredRows, 3)
}
func BenchmarkFilteredScrolling(b *testing.B) {
// Scrolling through a filtered table with many rows should be quick
// https://github.com/Evertras/bubble-table/issues/135
const rowCount = 40000
columns := []Column{NewColumn("title", "title", 10).WithFiltered(true)}
rows := make([]Row, rowCount)
for i := 0; i < rowCount; i++ {
rows[i] = NewRow(RowData{
"title": fmt.Sprintf("%d", i),
})
}
model := New(columns).WithRows(rows).Filtered(true)
model = model.WithFilterInputValue("1")
hitKey := func(key rune) {
model, _ = model.Update(
tea.KeyMsg{
Type: tea.KeyRunes,
Runes: []rune{key},
})
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
hitKey('j')
}
}
func BenchmarkFilteredScrollingPaged(b *testing.B) {
// Scrolling through a filtered table with many rows should be quick
// https://github.com/Evertras/bubble-table/issues/135
const rowCount = 40000
columns := []Column{NewColumn("title", "title", 10).WithFiltered(true)}
rows := make([]Row, rowCount)
for i := 0; i < rowCount; i++ {
rows[i] = NewRow(RowData{
"title": fmt.Sprintf("%d", i),
})
}
model := New(columns).WithRows(rows).Filtered(true).WithPageSize(50)
model = model.WithFilterInputValue("1")
b.ResetTimer()
for i := 0; i < b.N; i++ {
model, _ = model.Update(
tea.KeyMsg{
Type: tea.KeyRunes,
Runes: []rune{'j'},
})
}
}
func BenchmarkFilteredRenders(b *testing.B) {
// Rendering a filtered table should be fast
// https://github.com/Evertras/bubble-table/issues/135
const rowCount = 40000
columns := []Column{NewColumn("title", "title", 10).WithFiltered(true)}
rows := make([]Row, rowCount)
for i := 0; i < rowCount; i++ {
rows[i] = NewRow(RowData{
"title": fmt.Sprintf("%d", i),
})
}
model := New(columns).WithRows(rows).Filtered(true).WithPageSize(50)
model = model.WithFilterInputValue("1")
b.ResetTimer()
for i := 0; i < b.N; i++ {
// Don't care about result, just rendering
_ = model.View()
}
}
func TestFuzzyFilter_EmptyFilterMatchesAll(t *testing.T) {
cols := []Column{
NewColumn("name", "Name", 10).WithFiltered(true),
}
rows := []Row{
NewRow(RowData{"name": "Acme Steel"}),
NewRow(RowData{"name": "Globex"}),
}
for index, row := range rows {
if !filterFuncFuzzy(FilterFuncInput{
Columns: cols,
Row: row,
Filter: "",
}) {
t.Fatalf("row %d should match empty filter", index)
}
}
}
func TestFuzzyFilter_SubsequenceAcrossColumns(t *testing.T) {
cols := []Column{
NewColumn("name", "Name", 10).WithFiltered(true),
NewColumn("city", "City", 10).WithFiltered(true),
}
row := NewRow(RowData{
"name": "Acme",
"city": "Stuttgart",
})
type testCase struct {
name string
filter string
shouldMatch bool
}
testCases := []testCase{
{"subsequence match", "agt", true},
{"case-insensitive match", "ACM", true},
{"not a subsequence", "zzt", false},
}
for _, tc := range testCases {
assert.Equal(t, tc.shouldMatch, filterFuncFuzzy(FilterFuncInput{
Columns: cols,
Row: row,
Filter: tc.filter,
}))
}
}
func TestFuzzyFilter_ColumnNotInRow(t *testing.T) {
cols := []Column{
NewColumn("column_name_doesnt_match", "Name", 10).WithFiltered(true),
}
row := NewRow(RowData{
"name": "Acme Steel",
})
assert.False(t, filterFuncFuzzy(FilterFuncInput{
Columns: cols,
Row: row,
Filter: "steel",
}), "Shouldn't match")
}
func TestFuzzyFilter_RowHasEmptyHaystack(t *testing.T) {
cols := []Column{
NewColumn("name", "Name", 10).WithFiltered(true),
}
row := NewRow(RowData{"name": ""})
// literally any value other than an empty string
// should not match
assert.False(t, filterFuncFuzzy(FilterFuncInput{
Columns: cols,
Row: row,
Filter: "a",
}), "Shouldn't match")
}
func TestFuzzyFilter_MultiToken_AND(t *testing.T) {
cols := []Column{
NewColumn("name", "Name", 10).WithFiltered(true),
NewColumn("dept", "Dept", 10).WithFiltered(true),
}
row := NewRow(RowData{
"name": "Wayne Enterprises",
"dept": "R&D",
})
// Both tokens must match as subsequences somewhere in the concatenated haystack
assert.True(t, filterFuncFuzzy(FilterFuncInput{
Columns: cols,
Row: row,
Filter: "wy ent",
}), "Should match wy ent") // "wy" in Wayne, "ent" in Enterprises
assert.False(t, filterFuncFuzzy(FilterFuncInput{
Columns: cols,
Row: row,
Filter: "wy zzz",
}), "Shouldn't match wy zzz")
}
func TestFuzzyFilter_IgnoresNonFilterableColumns(t *testing.T) {
cols := []Column{
NewColumn("name", "Name", 10).WithFiltered(true),
NewColumn("secret", "Secret", 10).WithFiltered(false), // should be ignored
}
row := NewRow(RowData{
"name": "Acme",
"secret": "topsecretpattern",
})
assert.False(t, filterFuncFuzzy(FilterFuncInput{
Columns: cols,
Row: row,
Filter: "topsecret",
}), "Shouldn't match on non-filterable")
}
func TestFuzzyFilter_UnwrapsStyledCell(t *testing.T) {
cols := []Column{
NewColumn("name", "Name", 10).WithFiltered(true),
}
row := NewRow(RowData{
"name": NewStyledCell("Nakatomi Plaza", lipgloss.NewStyle()),
})
assert.True(t, filterFuncFuzzy(FilterFuncInput{
Columns: cols,
Row: row,
Filter: "nak plz",
}), "Expected fuzzy subsequence to match within StyledCell data")
}
func TestFuzzyFilter_NonStringValuesFormatted(t *testing.T) {
cols := []Column{
NewColumn("id", "ID", 6).WithFiltered(true),
}
row := NewRow(RowData{
"id": 12345, // should be formatted via fmt.Sprintf("%v", v)
})
assert.True(t, filterFuncFuzzy(FilterFuncInput{
Columns: cols,
Row: row,
Filter: "245", // subsequence of "12345"
}), "expected matcher to format non-strings and match subsequence")
}
func TestFuzzySubSequenceMatch_EmptyString(t *testing.T) {
assert.True(t, fuzzySubsequenceMatch("anything", ""), "empty needle should match anything")
assert.False(t, fuzzySubsequenceMatch("", "a"), "non-empty needle should not match empty haystack")
assert.True(t, fuzzySubsequenceMatch("", ""), "empty needle should match empty haystack")
}
================================================
FILE: table/footer.go
================================================
package table
import (
"fmt"
"strings"
)
func (m Model) hasFooter() bool {
return m.footerVisible && (m.staticFooter != "" || m.pageSize != 0 || m.filtered)
}
func (m Model) renderFooter(width int, includeTop bool) string {
if !m.hasFooter() {
return ""
}
const borderAdjustment = 2
styleFooter := m.baseStyle.Copy().Inherit(m.border.styleFooter).Width(width - borderAdjustment)
if includeTop {
styleFooter = styleFooter.BorderTop(true)
}
if m.staticFooter != "" {
return styleFooter.Render(m.staticFooter)
}
sections := []string{}
if m.filtered && (m.filterTextInput.Focused() || m.filterTextInput.Value() != "") {
sections = append(sections, m.filterTextInput.View())
}
// paged feature enabled
if m.pageSize != 0 {
str := fmt.Sprintf("%d/%d", m.CurrentPage(), m.MaxPages())
if m.filtered && m.filterTextInput.Focused() {
// Need to apply inline style here in case of filter input cursor, because
// the input cursor resets the style after rendering. Note that Inline(true)
// creates a copy, so it's safe to use here without mutating the underlying
// base style.
str = m.baseStyle.Inline(true).Render(str)
}
sections = append(sections, str)
}
footerText := strings.Join(sections, " ")
return styleFooter.Render(footerText)
}
================================================
FILE: table/header.go
================================================
package table
import "github.com/charmbracelet/lipgloss"
// This is long and could use some refactoring in the future, but unsure of how
// to pick it apart right now.
//
//nolint:funlen,cyclop
func (m Model) renderHeaders() string {
headerStrings := []string{}
totalRenderedWidth := 0
headerStyles := m.styleHeaders()
renderHeader := func(column Column, borderStyle lipgloss.Style) string {
borderStyle = borderStyle.Inherit(column.style).Inherit(m.baseStyle)
headerSection := limitStr(column.title, column.width)
return borderStyle.Render(headerSection)
}
for columnIndex, column := range m.columns {
var borderStyle lipgloss.Style
if m.horizontalScrollOffsetCol > 0 && columnIndex == m.horizontalScrollFreezeColumnsCount {
if columnIndex == 0 {
borderStyle = headerStyles.left.Copy()
} else {
borderStyle = headerStyles.inner.Copy()
}
rendered := renderHeader(genOverflowColumnLeft(1), borderStyle)
totalRenderedWidth += lipgloss.Width(rendered)
headerStrings = append(headerStrings, rendered)
}
if columnIndex >= m.horizontalScrollFreezeColumnsCount &&
columnIndex < m.horizontalScrollOffsetCol+m.horizontalScrollFreezeColumnsCount {
continue
}
if len(headerStrings) == 0 {
borderStyle = headerStyles.left.Copy()
} else if columnIndex < len(m.columns)-1 {
borderStyle = headerStyles.inner.Copy()
} else {
borderStyle = headerStyles.right.Copy()
}
rendered := renderHeader(column, borderStyle)
if m.maxTotalWidth != 0 {
renderedWidth := lipgloss.Width(rendered)
const (
borderAdjustment = 1
overflowColWidth = 2
)
targetWidth := m.maxTotalWidth - overflowColWidth
if columnIndex == len(m.columns)-1 {
// If this is the last header, we don't need to account for the
// overflow arrow column
targetWidth = m.maxTotalWidth
}
if totalRenderedWidth+renderedWidth > targetWidth {
overflowWidth := m.maxTotalWidth - totalRenderedWidth - borderAdjustment
overflowStyle := genOverflowStyle(headerStyles.right, overflowWidth)
overflowColumn := genOverflowColumnRight(overflowWidth)
overflowStr := renderHeader(overflowColumn, overflowStyle)
headerStrings = append(headerStrings, overflowStr)
break
}
totalRenderedWidth += renderedWidth
}
headerStrings = append(headerStrings, rendered)
}
headerBlock := lipgloss.JoinHorizontal(lipgloss.Bottom, headerStrings...)
return headerBlock
}
================================================
FILE: table/keys.go
================================================
package table
import "github.com/charmbracelet/bubbles/key"
// KeyMap defines the keybindings for the table when it's focused.
type KeyMap struct {
RowDown key.Binding
RowUp key.Binding
RowSelectToggle key.Binding
PageDown key.Binding
PageUp key.Binding
PageFirst key.Binding
PageLast key.Binding
// Filter allows the user to start typing and filter the rows.
Filter key.Binding
// FilterBlur is the key that stops the user's input from typing into the filter.
FilterBlur key.Binding
// FilterClear will clear the filter while it's blurred.
FilterClear key.Binding
// ScrollRight will move one column to the right when overflow occurs.
ScrollRight key.Binding
// ScrollLeft will move one column to the left when overflow occurs.
ScrollLeft key.Binding
}
// DefaultKeyMap returns a set of sensible defaults for controlling a focused table with help text.
func DefaultKeyMap() KeyMap {
return KeyMap{
RowDown: key.NewBinding(
key.WithKeys("down", "j"),
key.WithHelp("↓/j", "move down"),
),
RowUp: key.NewBinding(
key.WithKeys("up", "k"),
key.WithHelp("↑/k", "move up"),
),
RowSelectToggle: key.NewBinding(
key.WithKeys(" ", "enter"),
key.WithHelp("<space>/enter", "select row"),
),
PageDown: key.NewBinding(
key.WithKeys("right", "l", "pgdown"),
key.WithHelp("→/h/page down", "next page"),
),
PageUp: key.NewBinding(
key.WithKeys("left", "h", "pgup"),
key.WithHelp("←/h/page up", "previous page"),
),
PageFirst: key.NewBinding(
key.WithKeys("home", "g"),
key.WithHelp("home/g", "first page"),
),
PageLast: key.NewBinding(
key.WithKeys("end", "G"),
key.WithHelp("end/G", "last page"),
),
Filter: key.NewBinding(
key.WithKeys("/"),
key.WithHelp("/", "filter"),
),
FilterBlur: key.NewBinding(
key.WithKeys("enter", "esc"),
key.WithHelp("enter/esc", "unfocus"),
),
FilterClear: key.NewBinding(
key.WithKeys("esc"),
key.WithHelp("esc", "clear filter"),
),
ScrollRight: key.NewBinding(
key.WithKeys("shift+right"),
key.WithHelp("shift+→", "scroll right"),
),
ScrollLeft: key.NewBinding(
key.WithKeys("shift+left"),
key.WithHelp("shift+←", "scroll left"),
),
}
}
// FullHelp returns a multi row view of all the helpkeys that are defined. Needed to fullfil the 'help.Model' interface.
// Also appends all user defined extra keys to the help.
func (m Model) FullHelp() [][]key.Binding {
keyBinds := [][]key.Binding{
{m.keyMap.RowDown, m.keyMap.RowUp, m.keyMap.RowSelectToggle},
{m.keyMap.PageDown, m.keyMap.PageUp, m.keyMap.PageFirst, m.keyMap.PageLast},
{m.keyMap.Filter, m.keyMap.FilterBlur, m.keyMap.FilterClear, m.keyMap.ScrollRight, m.keyMap.ScrollLeft},
}
if m.additionalFullHelpKeys != nil {
keyBinds = append(keyBinds, m.additionalFullHelpKeys())
}
return keyBinds
}
// ShortHelp just returns a single row of help views. Needed to fullfil the 'help.Model' interface.
// Also appends all user defined extra keys to the help.
func (m Model) ShortHelp() []key.Binding {
keyBinds := []key.Binding{
m.keyMap.RowDown,
m.keyMap.RowUp,
m.keyMap.RowSelectToggle,
m.keyMap.PageDown,
m.keyMap.PageUp,
m.keyMap.Filter,
m.keyMap.FilterBlur,
m.keyMap.FilterClear,
}
if m.additionalShortHelpKeys != nil {
keyBinds = append(keyBinds, m.additionalShortHelpKeys()...)
}
return keyBinds
}
================================================
FILE: table/keys_test.go
================================================
package table
import (
"testing"
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/key"
"github.com/stretchr/testify/assert"
)
func TestKeyMapShortHelp(t *testing.T) {
columns := []Column{NewColumn("c1", "Column1", 10)}
model := New(columns)
km := DefaultKeyMap()
model.WithKeyMap(km)
assert.Nil(t, model.additionalShortHelpKeys)
assert.Equal(t, model.ShortHelp(), []key.Binding{
model.keyMap.RowDown,
model.keyMap.RowUp,
model.keyMap.RowSelectToggle,
model.keyMap.PageDown,
model.keyMap.PageUp,
model.keyMap.Filter,
model.keyMap.FilterBlur,
model.keyMap.FilterClear},
)
// Testing if the 'adding of keys' works too.
keys := []key.Binding{key.NewBinding(key.WithKeys("t"), key.WithHelp("t", "Testing additional keybinds"))}
model = model.WithAdditionalShortHelpKeys(keys)
assert.NotNil(t, model.additionalShortHelpKeys)
assert.Equal(t, model.ShortHelp(), []key.Binding{
model.keyMap.RowDown,
model.keyMap.RowUp,
model.keyMap.RowSelectToggle,
model.keyMap.PageDown,
model.keyMap.PageUp,
model.keyMap.Filter,
model.keyMap.FilterBlur,
model.keyMap.FilterClear,
key.NewBinding(
key.WithKeys("t"),
key.WithHelp("t",
"Testing additional keybinds"),
),
})
}
func TestKeyMapFullHelp(t *testing.T) {
columns := []Column{NewColumn("c1", "Column1", 10)}
model := New(columns)
km := DefaultKeyMap()
model.WithKeyMap(km)
assert.Nil(t, model.additionalFullHelpKeys)
assert.Equal(t,
model.FullHelp(),
[][]key.Binding{
{model.keyMap.RowDown, model.keyMap.RowUp, model.keyMap.RowSelectToggle},
{model.keyMap.PageDown, model.keyMap.PageUp, model.keyMap.PageFirst, model.keyMap.PageLast},
{
model.keyMap.Filter,
model.keyMap.FilterBlur,
model.keyMap.FilterClear,
model.keyMap.ScrollRight,
model.keyMap.ScrollLeft,
},
},
)
// Testing if the 'adding of keys' works too.
keys := []key.Binding{key.NewBinding(key.WithKeys("t"), key.WithHelp("t", "Testing additional keybinds"))}
model = model.WithAdditionalFullHelpKeys(keys)
assert.NotNil(t, model.additionalFullHelpKeys)
assert.Equal(t,
model.FullHelp(),
[][]key.Binding{
{model.keyMap.RowDown, model.keyMap.RowUp, model.keyMap.RowSelectToggle},
{model.keyMap.PageDown, model.keyMap.PageUp, model.keyMap.PageFirst, model.keyMap.PageLast},
{model.keyMap.Filter, model.keyMap.FilterBlur,
model.keyMap.FilterClear,
model.keyMap.ScrollRight,
model.keyMap.ScrollLeft},
{key.NewBinding(key.WithKeys("t"), key.WithHelp("t", "Testing additional keybinds"))}},
)
}
// Testing if Model actually implements the 'help.KeyMap' interface.
func TestKeyMapInterface(t *testing.T) {
model := New(nil)
assert.Implements(t, (*help.KeyMap)(nil), model)
}
================================================
FILE: table/model.go
================================================
package table
import (
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
const (
columnKeySelect = "___select___"
)
var (
defaultHighlightStyle = lipgloss.NewStyle().Background(lipgloss.Color("#334"))
)
// Model is the main table model. Create using New().
type Model struct {
// Data
columns []Column
rows []Row
metadata map[string]any
// Caches for optimizations
visibleRowCacheUpdated bool
visibleRowCache []Row
// Shown when data is missing from a row
missingDataIndicator any
// Interaction
focused bool
keyMap KeyMap
// Taken from: 'Bubbles/List'
// Additional key mappings for the short and full help views. This allows
// you to add additional key mappings to the help menu without
// re-implementing the help component. Of course, you can also disable the
// list's help component and implement a new one if you need more
// flexibility.
// You have to supply a keybinding like this:
// key.NewBinding( key.WithKeys("shift+left"), key.WithHelp("shift+←", "scroll left"))
// It needs both 'WithKeys' and 'WithHelp'
additionalShortHelpKeys func() []key.Binding
additionalFullHelpKeys func() []key.Binding
selectableRows bool
rowCursorIndex int
// Events
lastUpdateUserEvents []UserEvent
// Styles
baseStyle lipgloss.Style
highlightStyle lipgloss.Style
headerStyle lipgloss.Style
rowStyleFunc func(RowStyleFuncInput) lipgloss.Style
border Border
selectedText string
unselectedText string
// Header
headerVisible bool
// Footers
footerVisible bool
staticFooter string
// Pagination
pageSize int
currentPage int
paginationWrapping bool
// Sorting, where a stable sort is applied from first element to last so
// that elements are grouped by the later elements.
sortOrder []SortColumn
// Filter
filtered bool
filterTextInput textinput.Model
filterFunc FilterFunc
// For flex columns
targetTotalWidth int
// The maximum total width for overflow/scrolling
maxTotalWidth int
// Internal cached calculations for reference, may be higher than
// maxTotalWidth. If this is the case, we need to adjust the view
totalWidth int
// How far to scroll to the right, in columns
horizontalScrollOffsetCol int
// How many columns to freeze when scrolling horizontally
horizontalScrollFreezeColumnsCount int
// Calculated maximum column we can scroll to before the last is displayed
maxHorizontalColumnIndex int
// Minimum total height of the table
minimumHeight int
// Internal cached calculation, the height of the header and footer
// including borders. Used to determine how many padding rows to add.
metaHeight int
// If true, the table will be multiline
multiline bool
}
// New creates a new table ready for further modifications.
func New(columns []Column) Model {
filterInput := textinput.New()
filterInput.Prompt = "/"
model := Model{
columns: make([]Column, len(columns)),
metadata: make(map[string]any),
highlightStyle: defaultHighlightStyle.Copy(),
border: borderDefault,
headerVisible: true,
footerVisible: true,
keyMap: DefaultKeyMap(),
selectedText: "[x]",
unselectedText: "[ ]",
filterTextInput: filterInput,
filterFunc: filterFuncContains,
baseStyle: lipgloss.NewStyle().Align(lipgloss.Right),
paginationWrapping: true,
}
// Do a full deep copy to avoid unexpected edits
copy(model.columns, columns)
model.recalculateWidth()
return model
}
// Init initializes the table per the Bubble Tea architecture.
func (m Model) Init() tea.Cmd {
return nil
}
================================================
FILE: table/model_test.go
================================================
package table
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestModelInitReturnsNil(t *testing.T) {
model := New(nil)
cmd := model.Init()
assert.Nil(t, cmd)
}
================================================
FILE: table/options.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
}
================================================
FILE: table/options_test.go
================================================
package table
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestWithHighlightedRowSet(t *testing.T) {
highlightedIndex := 1
cols := []Column{
NewColumn("id", "ID", 3),
}
model := New(cols).WithRows([]Row{
NewRow(RowData{
"id": "first",
}),
NewRow(RowData{
"id": "second",
}),
}).WithHighlightedRow(highlightedIndex)
assert.Equal(t, model.rows[highlightedIndex], model.HighlightedRow())
}
func TestWithHighlightedRowSetNegative(t *testing.T) {
highlightedIndex := -1
cols := []Column{
NewColumn("id", "ID", 3),
}
model := New(cols).WithRows([]Row{
NewRow(RowData{
"id": "first",
}),
NewRow(RowData{
"id": "second",
}),
}).WithHighlightedRow(highlightedIndex)
assert.Equal(t, model.rows[0], model.HighlightedRow())
}
func TestWithHighlightedRowSetTooHigh(t *testing.T) {
highlightedIndex := 2
cols := []Column{
NewColumn("id", "ID", 3),
}
model := New(cols).WithRows([]Row{
NewRow(RowData{
"id": "first",
}),
NewRow(RowData{
"id": "second",
}),
}).WithHighlightedRow(highlightedIndex)
assert.Equal(t, model.rows[1], model.HighlightedRow())
}
// This is long only because it's a lot of repetitive test cases
//
//nolint:funlen
func TestPageOptions(t *testing.T) {
const (
pageSize = 5
rowCount = 30
)
cols := []Column{
NewColumn("id", "ID", 3),
}
rows := make([]Row, rowCount)
model := New(cols).WithRows(rows).WithPageSize(pageSize)
assert.Equal(t, 1, model.CurrentPage())
model = model.PageDown()
assert.Equal(t, 2, model.CurrentPage())
model = model.PageDown()
model = model.PageUp()
assert.Equal(t, 2, model.CurrentPage())
model = model.PageLast()
assert.Equal(t, 6, model.CurrentPage())
model = model.PageLast()
model = model.PageLast()
assert.Equal(t, 6, model.CurrentPage())
model = model.PageFirst()
assert.Equal(t, 1, model.CurrentPage())
model = model.PageFirst()
model = model.PageFirst()
assert.Equal(t, 1, model.CurrentPage())
model = model.PageUp()
assert.Equal(t, 6, model.CurrentPage())
model = model.PageDown()
assert.Equal(t, 1, model.CurrentPage())
model = model.WithCurrentPage(3)
model = model.WithCurrentPage(3)
model = model.WithCurrentPage(3)
assert.Equal(t, 3, model.CurrentPage())
assert.Equal(t, 10, model.rowCursorIndex)
model = model.WithCurrentPage(-1)
assert.Equal(t, 1, model.CurrentPage())
assert.Equal(t, 0, model.rowCursorIndex)
model = model.WithCurrentPage(0)
assert.Equal(t, 1, model.CurrentPage())
assert.Equal(t, 0, model.rowCursorIndex)
model = model.WithCurrentPage(7)
assert.Equal(t, 6, model.CurrentPage())
assert.Equal(t, 25, model.rowCursorIndex)
model.rowCursorIndex = 26
model = model.WithCurrentPage(6)
assert.Equal(t, 6, model.CurrentPage())
assert.Equal(t, 26, model.rowCursorIndex)
model = model.WithFooterVisibility(false)
assert.Equal(t, "", model.renderFooter(10, false))
model = model.WithFooterVisibility(true)
assert.Greater(t, len(model.renderFooter(10, false)), 10)
assert.Contains(t, model.renderFooter(10, false), "6/6")
}
func TestMinimumHeightOptions(t *testing.T) {
columns := []Column{
NewColumn("ka", "a", 3),
NewColumn("kb", "b", 4),
NewColumn("kc", "c", 5),
}
model := New(columns).WithMinimumHeight(10)
assert.Equal(t, 10, model.minimumHeight)
assert.Equal(t, 3, model.metaHeight)
model = model.WithPageSize(2)
assert.Equal(t, 5, model.metaHeight)
model = model.WithNoPagination()
assert.Equal(t, 3, model.metaHeight)
model = model.WithStaticFooter("footer with\nmultiple lines")
assert.Equal(t, 6, model.metaHeight)
model = model.WithStaticFooter("").Filtered(true)
assert.Equal(t, 5, model.metaHeight)
model = model.WithFooterVisibility(false)
assert.Equal(t, 3, model.metaHeight)
model = model.WithHeaderVisibility(false)
assert.Equal(t, 1, model.metaHeight)
}
// This is long only because the test cases are larger
//
//nolint:funlen
func TestSelectRowsProgramatically(t *testing.T) {
const col = "id"
tests := map[string]struct {
rows []Row
selectedIDs []int
}{
"no rows selected": {
[]Row{
NewRow(RowData{col: 1}),
NewRow(RowData{col: 2}),
NewRow(RowData{col: 3}),
},
[]int{},
},
"all rows selected": {
[]Row{
NewRow(RowData{col: 1}).Selected(true),
NewRow(RowData{col: 2}).Selected(true),
NewRow(RowData{col: 3}).Selected(true),
},
[]int{1, 2, 3},
},
"first row selected": {
[]Row{
NewRow(RowData{col: 1}).Selected(true),
NewRow(RowData{col: 2}),
NewRow(RowData{col: 3}),
},
[]int{1},
},
"last row selected": {
[]Row{
NewRow(RowData{col: 1}),
NewRow(RowData{col: 2}),
NewRow(RowData{col: 3}).Selected(true),
},
[]int{3},
},
}
baseModel := New([]Column{
NewColumn(col, col, 1),
})
for name, test := range tests {
t.Run(name, func(t *testing.T) {
model := baseModel.WithRows(test.rows)
sel := model.SelectedRows()
assert.Equal(t, len(test.selectedIDs), len(sel))
for i, id := range test.selectedIDs {
assert.Equal(t, id, sel[i].Data[col], "expecting row %d to have same %s column value", i)
}
model = model.WithAllRowsDeselected()
assert.Len(t, model.SelectedRows(), 0, "Did not deselect all rows")
})
}
}
func TestDefaultBorderIsDefault(t *testing.T) {
model := New([]Column{
NewColumn("id", "ID", 1),
}).WithRows([]Row{
NewRow(RowData{"id": 1}),
NewRow(RowData{"id": 2}),
NewRow(RowData{"id": 3}),
})
renderedInitial := model.View()
model = model.BorderRounded()
renderedRounded := model.View()
model = model.BorderDefault()
renderedDefault := model.View()
assert.NotEqual(t, renderedInitial, renderedRounded, "Rounded border should differ from default")
assert.Equal(t, renderedInitial, renderedDefault, "Default border should match initial state")
}
func BenchmarkSelectedRows(b *testing.B) {
const N = 1000
b.ReportAllocs()
rows := make([]Row, 0, N)
for i := 0; i < N; i++ {
rows = append(rows, NewRow(RowData{"row": i}).Selected(i%2 == 0))
}
model := New([]Column{
NewColumn("row", "Row", 4),
}).WithRows(rows)
var sel []Row
b.ResetTimer()
for i := 0; i < b.N; i++ {
sel = model.SelectedRows()
}
Rows = sel
}
var Rows []Row
================================================
FILE: table/overflow.go
================================================
package table
import "github.com/charmbracelet/lipgloss"
const columnKeyOverflowRight = "___overflow_r___"
const columnKeyOverflowLeft = "___overflow_l__"
func genOverflowStyle(base lipgloss.Style, width int) lipgloss.Style {
return base.Width(width).Align(lipgloss.Right)
}
func genOverflowColumnRight(width int) Column {
return NewColumn(columnKeyOverflowRight, ">", width)
}
func genOverflowColumnLeft(width int) Column {
return NewColumn(columnKeyOverflowLeft, "<", width)
}
================================================
FILE: table/pagination.go
================================================
package table
// PageSize returns the current page size for the table, or 0 if there is no
// pagination enabled.
func (m *Model) PageSize() int {
return m.pageSize
}
// CurrentPage returns the current page that the table is on, starting from an
// index of 1.
func (m *Model) CurrentPage() int {
return m.currentPage + 1
}
// MaxPages returns the maximum number of pages that are visible.
func (m *Model) MaxPages() int {
totalRows := len(m.GetVisibleRows())
if m.pageSize == 0 || totalRows == 0 {
return 1
}
return (totalRows-1)/m.pageSize + 1
}
// TotalRows returns the current total row count of the table. If the table is
// paginated, this is the total number of rows across all pages.
func (m *Model) TotalRows() int {
return len(m.GetVisibleRows())
}
// VisibleIndices returns the current visible rows by their 0 based index.
// Useful for custom pagination footers.
func (m *Model) VisibleIndices() (start, end int) {
totalRows := len(m.GetVisibleRows())
if m.pageSize == 0 {
start = 0
end = totalRows - 1
return start, end
}
start = m.pageSize * m.currentPage
end = start + m.pageSize - 1
if end >= totalRows {
end = totalRows - 1
}
return start, end
}
func (m *Model) pageDown() {
if m.pageSize == 0 || len(m.GetVisibleRows()) <= m.pageSize {
return
}
m.currentPage++
maxPageIndex := m.MaxPages() - 1
if m.currentPage > maxPageIndex {
if m.paginationWrapping {
m.currentPage = 0
} else {
m.currentPage = maxPageIndex
}
}
m.rowCursorIndex = m.currentPage * m.pageSize
}
func (m *Model) pageUp() {
if m.pageSize == 0 || len(m.GetVisibleRows()) <= m.pageSize {
return
}
m.currentPage--
maxPageIndex := m.MaxPages() - 1
if m.currentPage < 0 {
if m.paginationWrapping {
m.currentPage = maxPageIndex
} else {
m.currentPage = 0
}
}
m.rowCursorIndex = m.currentPage * m.pageSize
}
func (m *Model) pageFirst() {
m.currentPage = 0
m.rowCursorIndex = 0
}
func (m *Model) pageLast() {
m.currentPage = m.MaxPages() - 1
m.rowCursorIndex = m.currentPage * m.pageSize
}
func (m *Model) expectedPageForRowIndex(rowIndex int) int {
if m.pageSize == 0 {
return 0
}
expectedPage := rowIndex / m.pageSize
return expectedPage
}
================================================
FILE: table/pagination_test.go
================================================
package table
import (
"testing"
"github.com/stretchr/testify/assert"
)
func genPaginationTable(count, pageSize int) Model {
model := New([]Column{
NewColumn("id", "ID", 3),
})
rows := []Row{}
for i := 1; i <= count; i++ {
rows = append(rows, NewRow(RowData{
"id": i,
}))
}
return model.WithRows(rows).WithPageSize(pageSize)
}
func paginationRowID(row Row) int {
rowID, ok := row.Data["id"].(int)
if !ok {
panic("id not int, bad test")
}
return rowID
}
func getVisibleRows(m *Model) []Row {
start, end := m.VisibleIndices()
return m.GetVisibleRows()[start : end+1]
}
func TestPaginationAccessors(t *testing.T) {
const (
numRows = 100
pageSize = 20
)
model := genPaginationTable(numRows, pageSize)
assert.Equal(t, numRows, model.TotalRows())
assert.Equal(t, pageSize, model.PageSize())
}
func TestPaginationNoPageSizeReturnsAll(t *testing.T) {
const (
numRows = 100
pageSize = 0
)
model := genPaginationTable(numRows, pageSize)
paginatedRows := getVisibleRows(&model)
assert.Len(t, paginatedRows, numRows)
assert.Equal(t, 1, model.MaxPages())
}
func TestPaginationEmptyTableReturnsNoRows(t *testing.T) {
const (
numRows = 0
pageSize = 10
)
model := genPaginationTable(numRows, pageSize)
paginatedRows := getVisibleRows(&model)
assert.Len(t, paginatedRows, numRows)
}
func TestPaginationDefaultsToAllRows(t *testing.T) {
const numRows = 100
model := genPaginationTable(numRows, 0)
paginatedRows := getVisibleRows(&model)
assert.Len(t, paginatedRows, numRows)
}
func TestPaginationReturnsPartialFirstPage(t *testing.T) {
const (
numRows = 10
pageSize = 20
)
model := genPaginationTable(numRows, pageSize)
paginatedRows := getVisibleRows(&model)
assert.Len(t, paginatedRows, numRows)
}
func TestPaginationReturnsFirstFullPage(t *testing.T) {
const (
pageSize = 10
numRows = 20
)
model := genPaginationTable(numRows, pageSize)
paginatedRows := getVisibleRows(&model)
assert.Len(t, paginatedRows, pageSize)
for i, row := range paginatedRows {
assert.Equal(t, i+1, paginationRowID(row))
}
}
func TestPaginationReturnsSecondFullPageAfterMoving(t *testing.T) {
const (
pageSize = 10
numRows = 30
)
model := genPaginationTable(numRows, pageSize)
model.pageDown()
paginatedRows := getVisibleRows(&model)
assert.Len(t, paginatedRows, pageSize)
for i, row := range paginatedRows {
assert.Equal(t, i+11, paginationRowID(row))
}
}
func TestPaginationReturnsPartialFinalPage(t *testing.T) {
const (
pageSize = 10
numRows = 15
)
model := genPaginationTable(numRows, pageSize)
model.pageDown()
paginatedRows := getVisibleRows(&model)
assert.Len(t, paginatedRows, numRows-pageSize)
for i, row := range paginatedRows {
assert.Equal(t, i+11, paginationRowID(row))
}
}
func TestPaginationWrapsUpPartial(t *testing.T) {
const (
pageSize = 10
numRows = 15
)
model := genPaginationTable(numRows, pageSize)
model.pageUp()
paginatedRows := getVisibleRows(&model)
assert.Len(t, paginatedRows, numRows-pageSize)
for i, row := range paginatedRows {
assert.Equal(t, i+11, paginationRowID(row))
}
}
func TestPaginationWrapsUpFull(t *testing.T) {
const (
pageSize = 10
numRows = 20
)
model := genPaginationTable(numRows, pageSize)
model.pageUp()
paginatedRows := getVisibleRows(&model)
assert.Len(t, paginatedRows, numRows-pageSize)
for i, row := range paginatedRows {
assert.Equal(t, i+11, paginationRowID(row))
}
}
func TestPaginationWrapsUpSelf(t *testing.T) {
const (
pageSize = 10
numRows = 10
)
model := genPaginationTable(numRows, pageSize)
model.pageUp()
paginatedRows := getVisibleRows(&model)
assert.Len(t, paginatedRows, numRows)
for i, row := range paginatedRows {
assert.Equal(t, i+1, paginationRowID(row))
}
}
func TestPaginationWrapsDown(t *testing.T) {
const (
pageSize = 10
numRows = 15
)
model := genPaginationTable(numRows, pageSize)
model.pageDown()
model.pageDown()
paginatedRows := getVisibleRows(&model)
assert.Len(t, paginatedRows, pageSize)
for i, row := range paginatedRows {
assert.Equal(t, i+1, paginationRowID(row))
}
}
func TestPaginationWrapsDownSelf(t *testing.T) {
const (
pageSize = 10
numRows = 10
)
model := genPaginationTable(numRows, pageSize)
model.pageDown()
model.pageDown()
paginatedRows := getVisibleRows(&model)
assert.Len(t, paginatedRows, pageSize)
for i, row := range paginatedRows {
assert.Equal(t, i+1, paginationRowID(row))
}
}
func TestPaginationHighlightFirstOnPageDown(t *testing.T) {
const (
pageSize = 10
numRows = 20
)
model := genPaginationTable(numRows, pageSize)
assert.Equal(t, 1, paginationRowID(model.HighlightedRow()), "Initial test setup wrong, test code may be bad")
model.pageDown()
assert.Equal(t, 11, paginationRowID(model.HighlightedRow()), "Did not highlight expected row")
}
// This is long because of various test cases, not because of logic
//
//nolint:funlen
func TestExpectedPageForRowIndex(t *testing.T) {
tests := []struct {
name string
totalRows int
pageSize int
rowIndex int
expectedPage int
}{
{
name: "Empty",
},
{
name: "No pages",
totalRows: 50,
pageSize: 0,
rowIndex: 37,
expectedPage: 0,
},
{
name: "One page",
totalRows: 50,
pageSize: 50,
rowIndex: 37,
expectedPage: 0,
},
{
name: "First page",
totalRows: 50,
pageSize: 30,
rowIndex: 17,
expectedPage: 0,
},
{
name: "Second page",
totalRows: 50,
pageSize: 30,
rowIndex: 37,
expectedPage: 1,
},
{
name: "First page first row",
totalRows: 50,
pageSize: 30,
rowIndex: 0,
expectedPage: 0,
},
{
name: "First page last row",
totalRows: 50,
pageSize: 30,
rowIndex: 29,
expectedPage: 0,
},
{
name: "Second page first row",
totalRows: 50,
pageSize: 30,
rowIndex: 30,
expectedPage: 1,
},
{
name: "Second page last row",
totalRows: 50,
pageSize: 30,
rowIndex: 49,
expectedPage: 1,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
model := genPaginationTable(test.totalRows, test.pageSize)
page := model.expectedPageForRowIndex(test.rowIndex)
assert.Equal(t, test.expectedPage, page)
})
}
}
func TestClearPagination(t *testing.T) {
const (
pageSize = 10
numRows = 20
)
model := genPaginationTable(numRows, pageSize)
assert.Equal(t, 1, model.expectedPageForRowIndex(11))
model = model.WithNoPagination()
assert.Equal(t, 0, model.expectedPageForRowIndex(11))
}
func TestPaginationSetsLastPageWithFewerRows(t *testing.T) {
const (
pageSize = 10
numRowsOriginal = 30
numRowsAfter = 18
)
model := genPaginationTable(numRowsOriginal, pageSize)
model.pageUp()
assert.Equal(t, 3, model.CurrentPage())
rows := []Row{}
for i := 1; i <= numRowsAfter; i++ {
rows = append(rows, NewRow(RowData{
"id": i,
}))
}
model = model.WithRows(rows)
assert.Equal(t, 2, model.CurrentPage())
}
func TestPaginationBoundsToMaxPageOnResize(t *testing.T) {
const (
pageSize = 5
numRows = 20
)
model := genPaginationTable(numRows, pageSize)
assert.Equal(t, model.CurrentPage(), 1)
model.pageUp()
assert.Equal(t, model.CurrentPage(), 4)
model = model.WithPageSize(10)
assert.Equal(t, model.CurrentPage(), 2)
assert.Equal(t, model.MaxPages(), 2)
}
================================================
FILE: table/query.go
================================================
package table
// GetColumnSorting returns the current sorting rules for the table as a list of
// SortColumns, which are applied from first to last. This means that data will
// be grouped by the later elements in the list. The returned list is a copy
// and modifications will have no effect.
func (m *Model) GetColumnSorting() []SortColumn {
c := make([]SortColumn, len(m.sortOrder))
copy(c, m.sortOrder)
return c
}
// GetCanFilter returns true if the table enables filtering at all. This does
// not say whether a filter is currently active, only that the feature is enabled.
func (m *Model) GetCanFilter() bool {
return m.filtered
}
// GetIsFilterActive returns true if the table is currently being filtered. This
// does not say whether the table CAN be filtered, only whether or not a filter
// is actually currently being applied.
func (m *Model) GetIsFilterActive() bool {
return m.filterTextInput.Value() != ""
}
// GetIsFilterInputFocused returns true if the table's built-in filter input is
// currently focused.
func (m *Model) GetIsFilterInputFocused() bool {
return m.filterTextInput.Focused()
}
// GetCurrentFilter returns the current filter text being applied, or an empty
// string if none is applied.
func (m *Model) GetCurrentFilter() string {
return m.filterTextInput.Value()
}
// GetVisibleRows returns sorted and filtered rows.
func (m *Model) GetVisibleRows() []Row {
if m.visibleRowCacheUpdated {
return m.visibleRowCache
}
rows := make([]Row, len(m.rows))
copy(rows, m.rows)
if m.filtered {
rows = m.getFilteredRows(rows)
}
rows = getSortedRows(m.sortOrder, rows)
m.visibleRowCache = rows
m.visibleRowCacheUpdated = true
return rows
}
// GetHighlightedRowIndex returns the index of the Row that's currently highlighted
// by the user.
func (m *Model) GetHighlightedRowIndex() int {
return m.rowCursorIndex
}
// GetFocused returns whether or not the table is focused and is receiving inputs.
func (m *Model) GetFocused() bool {
return m.focused
}
// GetHorizontalScrollColumnOffset returns how many columns to the right the table
// has been scrolled. 0 means the table is all the way to the left, which is
// the starting default.
func (m *Model) GetHorizontalScrollColumnOffset() int {
return m.horizontalScrollOffsetCol
}
// GetHeaderVisibility returns true if the header has been set to visible (default)
// or false if the header has been set to hidden.
func (m *Model) GetHeaderVisibility() bool {
return m.headerVisible
}
// GetFooterVisibility returns true if the footer has been set to
// visible (default) or false if the footer has been set to hidden.
// Note that even if the footer is visible it will only be rendered if
// it has contents.
func (m *Model) GetFooterVisibility() bool {
return m.footerVisible
}
// GetPaginationWrapping returns true if pagination wrapping is enabled, or false
// if disabled. If disabled, navigating through pages will stop at the first
// and last pages.
func (m *Model) GetPaginationWrapping() bool {
return m.paginationWrapping
}
================================================
FILE: table/query_test.go
================================================
package table
import (
"testing"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/stretchr/testify/assert"
)
func TestGetColumnSorting(t *testing.T) {
cols := []Column{
NewColumn("a", "a", 3),
NewColumn("b", "b", 3),
NewColumn("c", "c", 3),
}
model := New(cols).SortByAsc("b")
sorted := model.GetColumnSorting()
assert.Len(t, sorted, 1, "Should only have one column")
assert.Equal(t, sorted[0].ColumnKey, "b", "Should sort column b")
assert.Equal(t, sorted[0].Direction, SortDirectionAsc, "Should be ascending")
sorted[0].Direction = SortDirectionDesc
assert.NotEqual(
t,
model.sortOrder[0].Direction,
sorted[0].Direction,
"Should not have been able to modify actual values",
)
}
func TestGetFilterData(t *testing.T) {
model := New([]Column{})
assert.False(t, model.GetIsFilterActive(), "Should not start with filter active")
assert.False(t, model.GetCanFilter(), "Should not start with filter ability")
assert.Equal(t, model.GetCurrentFilter(), "", "Filter string should be empty")
model = model.Filtered(true)
assert.False(t, model.GetIsFilterActive(), "Should not be filtered just because the ability was activated")
assert.True(t, model.GetCanFilter(), "Filter feature should be enabled")
assert.Equal(t, model.GetCurrentFilter(), "", "Filter string should be empty")
model.filterTextInput.SetValue("a")
assert.True(t, model.GetIsFilterActive(), "Typing anything into box should mark as filtered")
assert.True(t, model.GetCanFilter(), "Filter feature should be enabled")
assert.Equal(t, model.GetCurrentFilter(), "a", "Filter string should be what was typed")
}
func TestGetVisibleRows(t *testing.T) {
input := textinput.Model{}
input.SetValue("AAA")
columns := []Column{NewColumn("title", "title", 10).WithFiltered(true)}
rows := []Row{
NewRow(RowData{
"title": "AAA",
"description": "",
}),
NewRow(RowData{
"title": "BBB",
"description": "",
}),
NewRow(RowData{
"title": "CCC",
"description": "",
}),
}
m := Model{filtered: true, filterTextInput: input, columns: columns, rows: rows}
visibleRows := m.GetVisibleRows()
assert.Len(t, visibleRows, 1)
}
func TestGetHighlightedRowIndex(t *testing.T) {
model := New([]Column{})
assert.Equal(t, 0, model.GetHighlightedRowIndex(), "Empty table should still safely have 0 index highlighted")
// We don't actually need data to test this
empty := RowData{}
model = model.WithRows([]Row{NewRow(empty), NewRow(empty)})
assert.Equal(t, 0, model.GetHighlightedRowIndex(), "Unfocused table should start with 0 index")
model = model.WithHighlightedRow(1)
assert.Equal(t, 1, model.GetHighlightedRowIndex(), "Table with set highlighted row should return same highlighted row")
}
func TestGetFocused(t *testing.T) {
model := New([]Column{})
assert.Equal(t, false, model.GetFocused(), "Table should not be focused by default")
model = model.Focused(true)
assert.Equal(t, true, model.GetFocused(), "Table should be focused after being set")
}
func TestGetHorizontalScrollColumnOffset(t *testing.T) {
model := New([]Column{
NewColumn("1", "1", 4),
NewColumn("2", "2", 4),
NewColumn("3", "3", 4),
NewColumn("4", "4", 4),
}).
WithRows([]Row{
NewRow(RowData{
"1": "x1",
"2": "x2",
"3": "x3",
"4": "x4",
}),
}).
WithMaxTotalWidth(18).
Focused(true)
hitScrollRight := func() {
model, _ = model.Update(tea.KeyMsg{Type: tea.KeyShiftRight})
}
hitScrollLeft := func() {
model, _ = model.Update(tea.KeyMsg{Type: tea.KeyShiftLeft})
}
assert.Equal(
t,
0,
model.GetHorizontalScrollColumnOffset(),
"Should start to left",
)
hitScrollRight()
assert.Equal(
t,
1,
model.GetHorizontalScrollColumnOffset(),
"Should be 1 after scrolling to the right once",
)
hitScrollLeft()
assert.Equal(
t,
0,
model.GetHorizontalScrollColumnOffset(),
"Should be back to 0 after moving to the left",
)
hitScrollLeft()
assert.Equal(
t,
0,
model.GetHorizontalScrollColumnOffset(),
"Should still be 0 after trying to go left again",
)
}
func TestGetHeaderVisibility(t *testing.T) {
model := New([]Column{})
assert.True(t, model.GetHeaderVisibility(), "Header should be visible by default")
model = model.WithHeaderVisibility(false)
assert.False(t, model.GetHeaderVisibility(), "Header was not set to hidden")
}
func TestGetFooterVisibility(t *testing.T) {
model := New([]Column{})
assert.True(t, model.GetFooterVisibility(), "Footer should be visible by default")
model = model.WithFooterVisibility(false)
assert.False(t, model.GetFooterVisibility(), "Footer was not set to hidden")
}
func TestGetPaginationWrapping(t *testing.T) {
model := New([]Column{})
assert.True(t, model.GetPaginationWrapping(), "Pagination wrapping should default to true")
model = model.WithPaginationWrapping(false)
assert.False(t, model.GetPaginationWrapping(), "Pagination wrapping setting did not update after setting option")
}
func TestGetIsFilterInputFocused(t *testing.T) {
model := New([]Column{}).Filtered(true).Focused(true)
assert.False(t, model.GetIsFilterInputFocused(), "Text input shouldn't start focused")
model, _ = model.Update(tea.KeyMsg{
Type: tea.KeyRunes,
Runes: []rune{'/'},
})
assert.True(t, model.GetIsFilterInputFocused(), "Did not trigger text input")
model, _ = model.Update(tea.KeyMsg{
Type: tea.KeyEnter,
})
assert.False(t, model.GetIsFilterInputFocused(), "Should no longer be focused after hitting enter")
}
================================================
FILE: table/row.go
================================================
package table
import (
"fmt"
"sync/atomic"
"github.com/charmbracelet/lipgloss"
"github.com/muesli/reflow/wordwrap"
)
// RowData is a map of string column keys to arbitrary data. Data with a key
// that matches a column key will be displayed. Data with a key that does not
// match a column key will not be displayed, but will remain attached to the Row.
// This can be useful for attaching hidden metadata for future reference when
// retrieving r
gitextract_ij2y60qv/
├── .editorconfig
├── .github/
│ └── workflows/
│ ├── build.yml
│ ├── codeql-analysis.yml
│ ├── coverage.yml
│ └── lint.yml
├── .gitignore
├── .go-version
├── .golangci.yaml
├── CONTRIBUTING.md
├── LICENSE
├── Makefile
├── README.md
├── examples/
│ ├── dimensions/
│ │ ├── README.md
│ │ └── main.go
│ ├── events/
│ │ ├── README.md
│ │ └── main.go
│ ├── features/
│ │ ├── README.md
│ │ └── main.go
│ ├── filter/
│ │ ├── README.md
│ │ └── main.go
│ ├── filterapi/
│ │ ├── README.md
│ │ └── main.go
│ ├── flex/
│ │ ├── README.md
│ │ └── main.go
│ ├── metadata/
│ │ ├── README.md
│ │ └── main.go
│ ├── multiline/
│ │ ├── README.md
│ │ └── main.go
│ ├── pagination/
│ │ ├── README.md
│ │ └── main.go
│ ├── pokemon/
│ │ ├── README.md
│ │ └── main.go
│ ├── scrolling/
│ │ ├── README.md
│ │ └── main.go
│ ├── simplest/
│ │ ├── README.md
│ │ └── main.go
│ ├── sorting/
│ │ ├── README.md
│ │ └── main.go
│ └── updates/
│ ├── README.md
│ ├── data.go
│ └── main.go
├── flake.nix
├── go.mod
├── go.sum
└── table/
├── benchmarks_test.go
├── border.go
├── calc.go
├── calc_test.go
├── cell.go
├── column.go
├── column_test.go
├── data.go
├── data_test.go
├── dimensions.go
├── dimensions_test.go
├── doc.go
├── events.go
├── events_test.go
├── filter.go
├── filter_test.go
├── footer.go
├── header.go
├── keys.go
├── keys_test.go
├── model.go
├── model_test.go
├── options.go
├── options_test.go
├── overflow.go
├── pagination.go
├── pagination_test.go
├── query.go
├── query_test.go
├── row.go
├── scrolling.go
├── scrolling_fuzz_test.go
├── scrolling_test.go
├── sort.go
├── sort_test.go
├── strlimit.go
├── strlimit_test.go
├── update.go
├── update_test.go
├── view.go
├── view_selectable_test.go
└── view_test.go
SYMBOL INDEX (520 symbols across 56 files)
FILE: examples/dimensions/main.go
type Model (line 13) | type Model struct
method Init (line 55) | func (m Model) Init() tea.Cmd {
method Update (line 59) | func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 91) | func (m Model) View() string {
function genTable (line 21) | func genTable(columnCount int, rowCount int) table.Model {
function NewModel (line 45) | func NewModel() Model {
function main (line 113) | func main() {
FILE: examples/events/main.go
type Element (line 13) | type Element
constant columnKeyName (line 16) | columnKeyName = "name"
constant columnKeyElement (line 17) | columnKeyElement = "element"
constant columnKeyPokemonData (line 21) | columnKeyPokemonData = "pokedata"
constant elementNormal (line 23) | elementNormal Element = "Normal"
constant elementFire (line 24) | elementFire Element = "Fire"
constant elementElectric (line 25) | elementElectric Element = "Electric"
constant elementWater (line 26) | elementWater Element = "Water"
constant elementPlant (line 27) | elementPlant Element = "Plant"
type Pokemon (line 47) | type Pokemon struct
method ToRow (line 65) | func (p Pokemon) ToRow() table.Row {
function NewPokemon (line 55) | func NewPokemon(name string, element Element, conversationCount int, pos...
type Model (line 81) | type Model struct
method Init (line 120) | func (m Model) Init() tea.Cmd {
method Update (line 124) | func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 167) | func (m Model) View() string {
function NewModel (line 89) | func NewModel() Model {
function main (line 183) | func main() {
FILE: examples/features/main.go
constant columnKeyID (line 16) | columnKeyID = "id"
constant columnKeyName (line 17) | columnKeyName = "name"
constant columnKeyDescription (line 18) | columnKeyDescription = "description"
constant columnKeyCount (line 19) | columnKeyCount = "count"
type Model (line 44) | type Model struct
method Init (line 129) | func (m Model) Init() tea.Cmd {
method updateFooter (line 133) | func (m *Model) updateFooter() {
method Update (line 146) | func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 172) | func (m Model) View() string {
function NewModel (line 48) | func NewModel() Model {
function main (line 196) | func main() {
FILE: examples/filter/main.go
constant columnKeyTitle (line 12) | columnKeyTitle = "title"
constant columnKeyAuthor (line 13) | columnKeyAuthor = "author"
constant columnKeyDescription (line 14) | columnKeyDescription = "description"
type Model (line 17) | type Model struct
method Init (line 59) | func (m Model) Init() tea.Cmd {
method Update (line 63) | func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 86) | func (m Model) View() string {
function NewModel (line 21) | func NewModel() Model {
function main (line 97) | func main() {
FILE: examples/filterapi/main.go
constant columnKeyTitle (line 13) | columnKeyTitle = "title"
constant columnKeyAuthor (line 14) | columnKeyAuthor = "author"
constant columnKeyDescription (line 15) | columnKeyDescription = "description"
type Model (line 18) | type Model struct
method Init (line 63) | func (m Model) Init() tea.Cmd {
method Update (line 67) | func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 109) | func (m Model) View() string {
function NewModel (line 23) | func NewModel() Model {
function main (line 122) | func main() {
FILE: examples/flex/main.go
constant columnKeyName (line 13) | columnKeyName = "name"
constant columnKeyElement (line 14) | columnKeyElement = "element"
constant columnKeyDescription (line 15) | columnKeyDescription = "description"
constant minWidth (line 17) | minWidth = 30
constant minHeight (line 18) | minHeight = 8
constant fixedVerticalMargin (line 21) | fixedVerticalMargin = 4
type Model (line 24) | type Model struct
method Init (line 62) | func (m Model) Init() tea.Cmd {
method Update (line 66) | func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method recalculateTable (line 116) | func (m *Model) recalculateTable() {
method calculateWidth (line 122) | func (m Model) calculateWidth() int {
method calculateHeight (line 126) | func (m Model) calculateHeight() int {
method View (line 130) | func (m Model) View() string {
function NewModel (line 36) | func NewModel() Model {
function main (line 142) | func main() {
FILE: examples/metadata/main.go
type Element (line 13) | type Element
constant columnKeyName (line 16) | columnKeyName = "name"
constant columnKeyElement (line 17) | columnKeyElement = "element"
constant columnKeyConversations (line 18) | columnKeyConversations = "convos"
constant columnKeyPositiveSentiment (line 19) | columnKeyPositiveSentiment = "positive"
constant columnKeyNegativeSentiment (line 20) | columnKeyNegativeSentiment = "negative"
constant columnKeyPokemonData (line 24) | columnKeyPokemonData = "pokedata"
constant elementNormal (line 26) | elementNormal Element = "Normal"
constant elementFire (line 27) | elementFire Element = "Fire"
constant elementElectric (line 28) | elementElectric Element = "Electric"
constant elementWater (line 29) | elementWater Element = "Water"
constant elementPlant (line 30) | elementPlant Element = "Plant"
type Pokemon (line 50) | type Pokemon struct
method ToRow (line 68) | func (p Pokemon) ToRow() table.Row {
function NewPokemon (line 58) | func NewPokemon(name string, element Element, conversationCount int, pos...
type Model (line 87) | type Model struct
method Init (line 124) | func (m Model) Init() tea.Cmd {
method Update (line 128) | func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 148) | func (m Model) View() string {
function NewModel (line 91) | func NewModel() Model {
function main (line 163) | func main() {
FILE: examples/multiline/main.go
constant columnKeyName (line 13) | columnKeyName = "name"
constant columnKeyCountry (line 14) | columnKeyCountry = "country"
constant columnKeyCurrency (line 15) | columnKeyCurrency = "crurrency"
type Model (line 18) | type Model struct
method Init (line 125) | func (m Model) Init() tea.Cmd {
method Update (line 129) | func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 149) | func (m Model) View() string {
function NewModel (line 22) | func NewModel() Model {
function main (line 160) | func main() {
FILE: examples/pagination/main.go
type Model (line 14) | type Model struct
method regenTableRows (line 65) | func (m *Model) regenTableRows() {
method Init (line 70) | func (m Model) Init() tea.Cmd {
method Update (line 74) | func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 135) | func (m Model) View() string {
function genRows (line 21) | func genRows(columnCount int, rowCount int) []table.Row {
function genTable (line 38) | func genTable(columnCount int, rowCount int) table.Model {
function NewModel (line 51) | func NewModel() Model {
function main (line 152) | func main() {
FILE: examples/pokemon/main.go
constant columnKeyName (line 12) | columnKeyName = "name"
constant columnKeyElement (line 13) | columnKeyElement = "element"
constant columnKeyConversations (line 14) | columnKeyConversations = "convos"
constant columnKeyPositiveSentiment (line 15) | columnKeyPositiveSentiment = "positive"
constant columnKeyNegativeSentiment (line 16) | columnKeyNegativeSentiment = "negative"
constant colorNormal (line 18) | colorNormal = "#fa0"
constant colorElectric (line 19) | colorElectric = "#ff0"
constant colorFire (line 20) | colorFire = "#f64"
constant colorPlant (line 21) | colorPlant = "#8b8"
constant colorWater (line 22) | colorWater = "#44f"
type Model (line 34) | type Model struct
method Init (line 118) | func (m Model) Init() tea.Cmd {
method Update (line 122) | func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 150) | func (m Model) View() string {
function makeRow (line 54) | func makeRow(name, element string, numConversations int, positiveSentime...
function genMetadata (line 80) | func genMetadata(favoriteElementIndex int) map[string]any {
function NewModel (line 86) | func NewModel() Model {
function main (line 164) | func main() {
FILE: examples/scrolling/main.go
constant columnKeyID (line 13) | columnKeyID = "id"
constant numCols (line 15) | numCols = 100
constant numRows (line 16) | numRows = 10
constant idWidth (line 17) | idWidth = 5
constant colWidth (line 19) | colWidth = 3
constant maxWidth (line 20) | maxWidth = 30
type Model (line 23) | type Model struct
method Init (line 70) | func (m Model) Init() tea.Cmd {
method Update (line 74) | func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 94) | func (m Model) View() string {
function colKey (line 27) | func colKey(colNum int) string {
function genRow (line 31) | func genRow(id int) table.Row {
function NewModel (line 43) | func NewModel() Model {
function main (line 104) | func main() {
FILE: examples/simplest/main.go
constant columnKeyName (line 12) | columnKeyName = "name"
constant columnKeyElement (line 13) | columnKeyElement = "element"
type Model (line 16) | type Model struct
method Init (line 38) | func (m Model) Init() tea.Cmd {
method Update (line 42) | func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 62) | func (m Model) View() string {
function NewModel (line 20) | func NewModel() Model {
function main (line 72) | func main() {
FILE: examples/sorting/main.go
constant columnKeyName (line 12) | columnKeyName = "name"
constant columnKeyType (line 13) | columnKeyType = "type"
constant columnKeyWins (line 14) | columnKeyWins = "wins"
type Model (line 17) | type Model struct
method Init (line 66) | func (m Model) Init() tea.Cmd {
method Update (line 70) | func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 103) | func (m Model) View() string {
function NewModel (line 24) | func NewModel() Model {
function main (line 113) | func main() {
FILE: examples/updates/data.go
type SomeData (line 6) | type SomeData struct
method RandomizeScoreAndStatus (line 26) | func (s *SomeData) RandomizeScoreAndStatus() {
function NewSomeData (line 13) | func NewSomeData(id string) *SomeData {
FILE: examples/updates/main.go
constant columnKeyID (line 15) | columnKeyID = "id"
constant columnKeyScore (line 16) | columnKeyScore = "score"
constant columnKeyStatus (line 17) | columnKeyStatus = "status"
type Model (line 26) | type Model struct
method Init (line 95) | func (m Model) Init() tea.Cmd {
method Update (line 99) | func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 150) | func (m Model) View() string {
function rowStyleFunc (line 34) | func rowStyleFunc(input table.RowStyleFuncInput) lipgloss.Style {
function NewModel (line 55) | func NewModel() Model {
function refreshDataCmd (line 63) | func refreshDataCmd() tea.Msg {
function generateColumns (line 77) | func generateColumns(numCritical int) []table.Column {
function generateRowsFromData (line 166) | func generateRowsFromData(data []*SomeData) []table.Row {
function main (line 182) | func main() {
FILE: table/benchmarks_test.go
function benchTable (line 10) | func benchTable(numColumns, numDataRows int) Model {
function BenchmarkPlain3x3TableView (line 33) | func BenchmarkPlain3x3TableView(b *testing.B) {
function BenchmarkPlainTableViews (line 59) | func BenchmarkPlainTableViews(b *testing.B) {
FILE: table/border.go
type Border (line 6) | type Border struct
method generateStyles (line 102) | func (b *Border) generateStyles() {
method styleLeftWithFooter (line 117) | func (b *Border) styleLeftWithFooter(original lipgloss.Style) lipgloss...
method styleRightWithFooter (line 125) | func (b *Border) styleRightWithFooter(original lipgloss.Style) lipglos...
method styleBothWithFooter (line 133) | func (b *Border) styleBothWithFooter(original lipgloss.Style) lipgloss...
method generateMultiStyles (line 145) | func (b *Border) generateMultiStyles() {
method generateSingleColumnStyles (line 230) | func (b *Border) generateSingleColumnStyles() {
method generateSingleRowStyles (line 264) | func (b *Border) generateSingleRowStyles() {
method generateSingleCellStyle (line 302) | func (b *Border) generateSingleCellStyle() {
function init (line 97) | func init() {
method BorderDefault (line 320) | func (m Model) BorderDefault() Model {
method BorderRounded (line 328) | func (m Model) BorderRounded() Model {
method Border (line 336) | func (m Model) Border(border Border) Model {
type borderStyleRow (line 344) | type borderStyleRow struct
method inherit (line 350) | func (b *borderStyleRow) inherit(s lipgloss.Style) {
method styleHeaders (line 360) | func (m Model) styleHeaders() borderStyleRow {
method styleRows (line 409) | func (m Model) styleRows() (inner borderStyleRow, last borderStyleRow) {
FILE: table/calc.go
function min (line 6) | func min(x, y int) int {
function max (line 17) | func max(x, y int) int {
function gcd (line 28) | func gcd(x, y int) int {
FILE: table/calc_test.go
function TestMin (line 12) | func TestMin(t *testing.T) {
function TestMax (line 43) | func TestMax(t *testing.T) {
function TestGCD (line 74) | func TestGCD(t *testing.T) {
FILE: table/cell.go
type StyledCell (line 10) | type StyledCell struct
type StyledCellFuncInput (line 25) | type StyledCellFuncInput struct
function NewStyledCell (line 46) | func NewStyledCell(data any, style lipgloss.Style) StyledCell {
function NewStyledCellWithStyleFunc (line 55) | func NewStyledCellWithStyleFunc(data any, styleFunc StyledCellFunc) Styl...
FILE: table/column.go
type Column (line 8) | type Column struct
method WithStyle (line 49) | func (c Column) WithStyle(style lipgloss.Style) Column {
method WithFiltered (line 57) | func (c Column) WithFiltered(filterable bool) Column {
method WithFormatString (line 70) | func (c Column) WithFormatString(fmtString string) Column {
method isFlex (line 76) | func (c *Column) isFlex() bool {
method Title (line 81) | func (c Column) Title() string {
method Key (line 86) | func (c Column) Key() string {
method Width (line 91) | func (c Column) Width() int {
method FlexFactor (line 96) | func (c Column) FlexFactor() int {
method IsFlex (line 101) | func (c Column) IsFlex() bool {
method Filterable (line 106) | func (c Column) Filterable() bool {
method Style (line 111) | func (c Column) Style() lipgloss.Style {
method FmtString (line 116) | func (c Column) FmtString() string {
function NewColumn (line 22) | func NewColumn(key, title string, width int) Column {
function NewFlexColumn (line 39) | func NewFlexColumn(key, title string, flexFactor int) Column {
FILE: table/column_test.go
function TestColumnTitle (line 11) | func TestColumnTitle(t *testing.T) {
function TestColumnKey (line 34) | func TestColumnKey(t *testing.T) {
function TestColumnWidth (line 57) | func TestColumnWidth(t *testing.T) {
function TestColumnFlexFactor (line 80) | func TestColumnFlexFactor(t *testing.T) {
function TestColumnIsFlex (line 103) | func TestColumnIsFlex(t *testing.T) {
function TestColumnFilterable (line 147) | func TestColumnFilterable(t *testing.T) {
function TestColumnStyle (line 171) | func TestColumnStyle(t *testing.T) {
function TestColumnFormatString (line 195) | func TestColumnFormatString(t *testing.T) {
FILE: table/data.go
function asInt (line 8) | func asInt(data any) (int64, bool) {
function asNumber (line 52) | func asNumber(data any) (float64, bool) {
FILE: table/data_test.go
function TestAsInt (line 10) | func TestAsInt(t *testing.T) {
function TestAsNumber (line 32) | func TestAsNumber(t *testing.T) {
FILE: table/dimensions.go
method recalculateWidth (line 7) | func (m *Model) recalculateWidth() {
function updateColumnWidths (line 27) | func updateColumnWidths(cols []Column, totalWidth int) {
method recalculateHeight (line 79) | func (m *Model) recalculateHeight() {
method calculatePadding (line 95) | func (m *Model) calculatePadding(numRows int) int {
FILE: table/dimensions_test.go
function TestColumnUpdateWidths (line 13) | func TestColumnUpdateWidths(t *testing.T) {
function TestRecalculateHeight (line 156) | func TestRecalculateHeight(t *testing.T) {
FILE: table/events.go
type UserEvent (line 6) | type UserEvent
method appendUserEvent (line 8) | func (m *Model) appendUserEvent(e UserEvent) {
method clearUserEvents (line 12) | func (m *Model) clearUserEvents() {
method GetLastUpdateUserEvents (line 19) | func (m *Model) GetLastUpdateUserEvents() []UserEvent {
type UserEventHighlightedIndexChanged (line 36) | type UserEventHighlightedIndexChanged struct
type UserEventRowSelectToggled (line 47) | type UserEventRowSelectToggled struct
type UserEventFilterInputFocused (line 55) | type UserEventFilterInputFocused struct
type UserEventFilterInputUnfocused (line 60) | type UserEventFilterInputUnfocused struct
FILE: table/events_test.go
function TestUserEventsEmptyWhenNothingHappens (line 10) | func TestUserEventsEmptyWhenNothingHappens(t *testing.T) {
function TestUserEventHighlightedIndexChanged (line 25) | func TestUserEventHighlightedIndexChanged(t *testing.T) {
function TestUserEventRowSelectToggled (line 102) | func TestUserEventRowSelectToggled(t *testing.T) {
function TestFilterFocusEvents (line 176) | func TestFilterFocusEvents(t *testing.T) {
FILE: table/filter.go
type FilterFuncInput (line 10) | type FilterFuncInput struct
type FilterFunc (line 26) | type FilterFunc
method getFilteredRows (line 28) | func (m Model) getFilteredRows(rows []Row) []Row {
function filterFuncContains (line 60) | func filterFuncContains(input FilterFuncInput) bool {
function filterFuncFuzzy (line 110) | func filterFuncFuzzy(input FilterFuncInput) bool {
function fuzzySubsequenceMatch (line 148) | func fuzzySubsequenceMatch(haystack, needle string) bool {
FILE: table/filter_test.go
function TestIsRowMatched (line 14) | func TestIsRowMatched(t *testing.T) {
function TestIsRowMatchedForNonStringer (line 69) | func TestIsRowMatchedForNonStringer(t *testing.T) {
function TestGetFilteredRowsNoColumnFiltered (line 101) | func TestGetFilteredRowsNoColumnFiltered(t *testing.T) {
function TestGetFilteredRowsUnfiltered (line 126) | func TestGetFilteredRowsUnfiltered(t *testing.T) {
function TestGetFilteredRowsFiltered (line 144) | func TestGetFilteredRowsFiltered(t *testing.T) {
function TestGetFilteredRowsRefocusAfterFilter (line 166) | func TestGetFilteredRowsRefocusAfterFilter(t *testing.T) {
function TestFilterWithExternalTextInput (line 215) | func TestFilterWithExternalTextInput(t *testing.T) {
function TestFilterWithSetValue (line 244) | func TestFilterWithSetValue(t *testing.T) {
function TestFilterFunc (line 281) | func TestFilterFunc(t *testing.T) {
function BenchmarkFilteredScrolling (line 328) | func BenchmarkFilteredScrolling(b *testing.B) {
function BenchmarkFilteredScrollingPaged (line 359) | func BenchmarkFilteredScrollingPaged(b *testing.B) {
function BenchmarkFilteredRenders (line 386) | func BenchmarkFilteredRenders(b *testing.B) {
function TestFuzzyFilter_EmptyFilterMatchesAll (line 410) | func TestFuzzyFilter_EmptyFilterMatchesAll(t *testing.T) {
function TestFuzzyFilter_SubsequenceAcrossColumns (line 430) | func TestFuzzyFilter_SubsequenceAcrossColumns(t *testing.T) {
function TestFuzzyFilter_ColumnNotInRow (line 461) | func TestFuzzyFilter_ColumnNotInRow(t *testing.T) {
function TestFuzzyFilter_RowHasEmptyHaystack (line 476) | func TestFuzzyFilter_RowHasEmptyHaystack(t *testing.T) {
function TestFuzzyFilter_MultiToken_AND (line 491) | func TestFuzzyFilter_MultiToken_AND(t *testing.T) {
function TestFuzzyFilter_IgnoresNonFilterableColumns (line 514) | func TestFuzzyFilter_IgnoresNonFilterableColumns(t *testing.T) {
function TestFuzzyFilter_UnwrapsStyledCell (line 531) | func TestFuzzyFilter_UnwrapsStyledCell(t *testing.T) {
function TestFuzzyFilter_NonStringValuesFormatted (line 546) | func TestFuzzyFilter_NonStringValuesFormatted(t *testing.T) {
function TestFuzzySubSequenceMatch_EmptyString (line 561) | func TestFuzzySubSequenceMatch_EmptyString(t *testing.T) {
FILE: table/footer.go
method hasFooter (line 8) | func (m Model) hasFooter() bool {
method renderFooter (line 12) | func (m Model) renderFooter(width int, includeTop bool) string {
FILE: table/header.go
method renderHeaders (line 9) | func (m Model) renderHeaders() string {
FILE: table/keys.go
type KeyMap (line 6) | type KeyMap struct
function DefaultKeyMap (line 34) | func DefaultKeyMap() KeyMap {
method FullHelp (line 89) | func (m Model) FullHelp() [][]key.Binding {
method ShortHelp (line 104) | func (m Model) ShortHelp() []key.Binding {
FILE: table/keys_test.go
function TestKeyMapShortHelp (line 11) | func TestKeyMapShortHelp(t *testing.T) {
function TestKeyMapFullHelp (line 48) | func TestKeyMapFullHelp(t *testing.T) {
function TestKeyMapInterface (line 86) | func TestKeyMapInterface(t *testing.T) {
FILE: table/model.go
constant columnKeySelect (line 11) | columnKeySelect = "___select___"
type Model (line 19) | type Model struct
method Init (line 146) | func (m Model) Init() tea.Cmd {
function New (line 115) | func New(columns []Column) Model {
FILE: table/model_test.go
function TestModelInitReturnsNil (line 9) | func TestModelInitReturnsNil(t *testing.T) {
FILE: table/options.go
type RowStyleFuncInput (line 15) | type RowStyleFuncInput struct
method WithRowStyleFunc (line 31) | func (m Model) WithRowStyleFunc(f func(RowStyleFuncInput) lipgloss.Style...
method WithHighlightedRow (line 38) | func (m Model) WithHighlightedRow(index int) Model {
method HeaderStyle (line 55) | func (m Model) HeaderStyle(style lipgloss.Style) Model {
method WithRows (line 62) | func (m Model) WithRows(rows []Row) Model {
method WithKeyMap (line 87) | func (m Model) WithKeyMap(keyMap KeyMap) Model {
method KeyMap (line 94) | func (m Model) KeyMap() KeyMap {
method SelectableRows (line 100) | func (m Model) SelectableRows(selectable bool) Model {
method HighlightedRow (line 121) | func (m Model) HighlightedRow() Row {
method SelectedRows (line 131) | func (m Model) SelectedRows() []Row {
method HighlightStyle (line 146) | func (m Model) HighlightStyle(style lipgloss.Style) Model {
method Focused (line 154) | func (m Model) Focused(focused bool) Model {
method Filtered (line 161) | func (m Model) Filtered(filtered bool) Model {
method StartFilterTyping (line 173) | func (m Model) StartFilterTyping() Model {
method WithStaticFooter (line 180) | func (m Model) WithStaticFooter(footer string) Model {
method WithPageSize (line 192) | func (m Model) WithPageSize(pageSize int) Model {
method WithNoPagination (line 209) | func (m Model) WithNoPagination() Model {
method WithPaginationWrapping (line 221) | func (m Model) WithPaginationWrapping(wrapping bool) Model {
method WithSelectedText (line 229) | func (m Model) WithSelectedText(unselected, selected string) Model {
method WithBaseStyle (line 243) | func (m Model) WithBaseStyle(style lipgloss.Style) Model {
method WithTargetWidth (line 252) | func (m Model) WithTargetWidth(totalWidth int) Model {
method WithMinimumHeight (line 261) | func (m Model) WithMinimumHeight(minimumHeight int) Model {
method PageDown (line 271) | func (m Model) PageDown() Model {
method PageUp (line 279) | func (m Model) PageUp() Model {
method PageLast (line 286) | func (m Model) PageLast() Model {
method PageFirst (line 293) | func (m Model) PageFirst() Model {
method WithCurrentPage (line 302) | func (m Model) WithCurrentPage(currentPage int) Model {
method WithColumns (line 323) | func (m Model) WithColumns(columns []Column) Model {
method WithFilterInput (line 341) | func (m Model) WithFilterInput(input textinput.Model) Model {
method WithFilterInputValue (line 355) | func (m Model) WithFilterInputValue(value string) Model {
method WithFilterFunc (line 371) | func (m Model) WithFilterFunc(shouldInclude FilterFunc) Model {
method WithFuzzyFilter (line 380) | func (m Model) WithFuzzyFilter() Model {
method WithFooterVisibility (line 385) | func (m Model) WithFooterVisibility(visibility bool) Model {
method WithHeaderVisibility (line 396) | func (m Model) WithHeaderVisibility(visibility bool) Model {
method WithMaxTotalWidth (line 410) | func (m Model) WithMaxTotalWidth(maxTotalWidth int) Model {
method WithHorizontalFreezeColumnCount (line 421) | func (m Model) WithHorizontalFreezeColumnCount(columnsToFreeze int) Model {
method ScrollRight (line 430) | func (m Model) ScrollRight() Model {
method ScrollLeft (line 437) | func (m Model) ScrollLeft() Model {
method WithMissingDataIndicator (line 447) | func (m Model) WithMissingDataIndicator(str string) Model {
method WithMissingDataIndicatorStyled (line 457) | func (m Model) WithMissingDataIndicatorStyled(styled StyledCell) Model {
method WithAllRowsDeselected (line 464) | func (m Model) WithAllRowsDeselected() Model {
method WithMultiline (line 479) | func (m Model) WithMultiline(multiline bool) Model {
method WithAdditionalShortHelpKeys (line 486) | func (m Model) WithAdditionalShortHelpKeys(keys []key.Binding) Model {
method WithAdditionalFullHelpKeys (line 495) | func (m Model) WithAdditionalFullHelpKeys(keys []key.Binding) Model {
method WithGlobalMetadata (line 506) | func (m Model) WithGlobalMetadata(metadata map[string]any) Model {
FILE: table/options_test.go
function TestWithHighlightedRowSet (line 9) | func TestWithHighlightedRowSet(t *testing.T) {
function TestWithHighlightedRowSetNegative (line 28) | func TestWithHighlightedRowSetNegative(t *testing.T) {
function TestWithHighlightedRowSetTooHigh (line 47) | func TestWithHighlightedRowSetTooHigh(t *testing.T) {
function TestPageOptions (line 69) | func TestPageOptions(t *testing.T) {
function TestMinimumHeightOptions (line 142) | func TestMinimumHeightOptions(t *testing.T) {
function TestSelectRowsProgramatically (line 175) | func TestSelectRowsProgramatically(t *testing.T) {
function TestDefaultBorderIsDefault (line 239) | func TestDefaultBorderIsDefault(t *testing.T) {
function BenchmarkSelectedRows (line 260) | func BenchmarkSelectedRows(b *testing.B) {
FILE: table/overflow.go
constant columnKeyOverflowRight (line 5) | columnKeyOverflowRight = "___overflow_r___"
constant columnKeyOverflowLeft (line 6) | columnKeyOverflowLeft = "___overflow_l__"
function genOverflowStyle (line 8) | func genOverflowStyle(base lipgloss.Style, width int) lipgloss.Style {
function genOverflowColumnRight (line 12) | func genOverflowColumnRight(width int) Column {
function genOverflowColumnLeft (line 16) | func genOverflowColumnLeft(width int) Column {
FILE: table/pagination.go
method PageSize (line 5) | func (m *Model) PageSize() int {
method CurrentPage (line 11) | func (m *Model) CurrentPage() int {
method MaxPages (line 16) | func (m *Model) MaxPages() int {
method TotalRows (line 28) | func (m *Model) TotalRows() int {
method VisibleIndices (line 34) | func (m *Model) VisibleIndices() (start, end int) {
method pageDown (line 54) | func (m *Model) pageDown() {
method pageUp (line 74) | func (m *Model) pageUp() {
method pageFirst (line 94) | func (m *Model) pageFirst() {
method pageLast (line 99) | func (m *Model) pageLast() {
method expectedPageForRowIndex (line 104) | func (m *Model) expectedPageForRowIndex(rowIndex int) int {
FILE: table/pagination_test.go
function genPaginationTable (line 9) | func genPaginationTable(count, pageSize int) Model {
function paginationRowID (line 25) | func paginationRowID(row Row) int {
function getVisibleRows (line 35) | func getVisibleRows(m *Model) []Row {
function TestPaginationAccessors (line 41) | func TestPaginationAccessors(t *testing.T) {
function TestPaginationNoPageSizeReturnsAll (line 53) | func TestPaginationNoPageSizeReturnsAll(t *testing.T) {
function TestPaginationEmptyTableReturnsNoRows (line 67) | func TestPaginationEmptyTableReturnsNoRows(t *testing.T) {
function TestPaginationDefaultsToAllRows (line 80) | func TestPaginationDefaultsToAllRows(t *testing.T) {
function TestPaginationReturnsPartialFirstPage (line 90) | func TestPaginationReturnsPartialFirstPage(t *testing.T) {
function TestPaginationReturnsFirstFullPage (line 103) | func TestPaginationReturnsFirstFullPage(t *testing.T) {
function TestPaginationReturnsSecondFullPageAfterMoving (line 120) | func TestPaginationReturnsSecondFullPageAfterMoving(t *testing.T) {
function TestPaginationReturnsPartialFinalPage (line 139) | func TestPaginationReturnsPartialFinalPage(t *testing.T) {
function TestPaginationWrapsUpPartial (line 158) | func TestPaginationWrapsUpPartial(t *testing.T) {
function TestPaginationWrapsUpFull (line 177) | func TestPaginationWrapsUpFull(t *testing.T) {
function TestPaginationWrapsUpSelf (line 196) | func TestPaginationWrapsUpSelf(t *testing.T) {
function TestPaginationWrapsDown (line 215) | func TestPaginationWrapsDown(t *testing.T) {
function TestPaginationWrapsDownSelf (line 235) | func TestPaginationWrapsDownSelf(t *testing.T) {
function TestPaginationHighlightFirstOnPageDown (line 255) | func TestPaginationHighlightFirstOnPageDown(t *testing.T) {
function TestExpectedPageForRowIndex (line 273) | func TestExpectedPageForRowIndex(t *testing.T) {
function TestClearPagination (line 353) | func TestClearPagination(t *testing.T) {
function TestPaginationSetsLastPageWithFewerRows (line 368) | func TestPaginationSetsLastPageWithFewerRows(t *testing.T) {
function TestPaginationBoundsToMaxPageOnResize (line 393) | func TestPaginationBoundsToMaxPageOnResize(t *testing.T) {
FILE: table/query.go
method GetColumnSorting (line 7) | func (m *Model) GetColumnSorting() []SortColumn {
method GetCanFilter (line 17) | func (m *Model) GetCanFilter() bool {
method GetIsFilterActive (line 24) | func (m *Model) GetIsFilterActive() bool {
method GetIsFilterInputFocused (line 30) | func (m *Model) GetIsFilterInputFocused() bool {
method GetCurrentFilter (line 36) | func (m *Model) GetCurrentFilter() string {
method GetVisibleRows (line 41) | func (m *Model) GetVisibleRows() []Row {
method GetHighlightedRowIndex (line 61) | func (m *Model) GetHighlightedRowIndex() int {
method GetFocused (line 66) | func (m *Model) GetFocused() bool {
method GetHorizontalScrollColumnOffset (line 73) | func (m *Model) GetHorizontalScrollColumnOffset() int {
method GetHeaderVisibility (line 79) | func (m *Model) GetHeaderVisibility() bool {
method GetFooterVisibility (line 87) | func (m *Model) GetFooterVisibility() bool {
method GetPaginationWrapping (line 94) | func (m *Model) GetPaginationWrapping() bool {
FILE: table/query_test.go
function TestGetColumnSorting (line 11) | func TestGetColumnSorting(t *testing.T) {
function TestGetFilterData (line 36) | func TestGetFilterData(t *testing.T) {
function TestGetVisibleRows (line 56) | func TestGetVisibleRows(t *testing.T) {
function TestGetHighlightedRowIndex (line 79) | func TestGetHighlightedRowIndex(t *testing.T) {
function TestGetFocused (line 95) | func TestGetFocused(t *testing.T) {
function TestGetHorizontalScrollColumnOffset (line 105) | func TestGetHorizontalScrollColumnOffset(t *testing.T) {
function TestGetHeaderVisibility (line 164) | func TestGetHeaderVisibility(t *testing.T) {
function TestGetFooterVisibility (line 174) | func TestGetFooterVisibility(t *testing.T) {
function TestGetPaginationWrapping (line 184) | func TestGetPaginationWrapping(t *testing.T) {
function TestGetIsFilterInputFocused (line 194) | func TestGetIsFilterInputFocused(t *testing.T) {
FILE: table/row.go
type RowData (line 16) | type RowData
type Row (line 20) | type Row struct
method WithStyle (line 50) | func (r Row) WithStyle(style lipgloss.Style) Row {
method Selected (line 248) | func (r Row) Selected(selected bool) Row {
function NewRow (line 33) | func NewRow(data RowData) Row {
method renderRowColumnData (line 57) | func (m Model) renderRowColumnData(row Row, column Column, rowStyle lipg...
method renderRow (line 123) | func (m Model) renderRow(rowIndex int, last bool) string {
method renderBlankRow (line 144) | func (m Model) renderBlankRow(last bool) string {
method renderRowData (line 152) | func (m Model) renderRowData(row Row, rowStyle lipgloss.Style, last bool...
FILE: table/scrolling.go
method scrollRight (line 3) | func (m *Model) scrollRight() {
method scrollLeft (line 9) | func (m *Model) scrollLeft() {
method recalculateLastHorizontalColumn (line 15) | func (m *Model) recalculateLastHorizontalColumn() {
FILE: table/scrolling_fuzz_test.go
function FuzzHorizontalScrollingStopEdgeCases (line 18) | func FuzzHorizontalScrollingStopEdgeCases(f *testing.F) {
FILE: table/scrolling_test.go
function TestHorizontalScrolling (line 12) | func TestHorizontalScrolling(t *testing.T) {
function TestHorizontalScrollWithFooter (line 64) | func TestHorizontalScrollWithFooter(t *testing.T) {
function TestHorizontalScrollingWithFooterAndFrozenCols (line 122) | func TestHorizontalScrollingWithFooterAndFrozenCols(t *testing.T) {
function TestHorizontalScrollStopsAtLastColumnBeingVisible (line 183) | func TestHorizontalScrollStopsAtLastColumnBeingVisible(t *testing.T) {
function TestNoScrollingWhenEntireTableIsVisible (line 247) | func TestNoScrollingWhenEntireTableIsVisible(t *testing.T) {
function TestHorizontalScrollingStopEdgeCases (line 289) | func TestHorizontalScrollingStopEdgeCases(t *testing.T) {
function TestHorizontalScrollingWithCustomKeybind (line 369) | func TestHorizontalScrollingWithCustomKeybind(t *testing.T) {
FILE: table/sort.go
type SortDirection (line 9) | type SortDirection
constant SortDirectionAsc (line 13) | SortDirectionAsc SortDirection = iota
constant SortDirectionDesc (line 16) | SortDirectionDesc
type SortColumn (line 20) | type SortColumn struct
method SortByAsc (line 29) | func (m Model) SortByAsc(columnKey string) Model {
method SortByDesc (line 46) | func (m Model) SortByDesc(columnKey string) Model {
method ThenSortByAsc (line 61) | func (m Model) ThenSortByAsc(columnKey string) Model {
method ThenSortByDesc (line 76) | func (m Model) ThenSortByDesc(columnKey string) Model {
type sortableTable (line 89) | type sortableTable struct
method Len (line 94) | func (s *sortableTable) Len() int {
method Swap (line 98) | func (s *sortableTable) Swap(i, j int) {
method extractString (line 104) | func (s *sortableTable) extractString(i int, column string) string {
method extractNumber (line 123) | func (s *sortableTable) extractNumber(i int, column string) (float64, ...
method Less (line 133) | func (s *sortableTable) Less(first, second int) bool {
function getSortedRows (line 155) | func getSortedRows(sortOrder []SortColumn, rows []Row) []Row {
FILE: table/sort_test.go
function TestSortSingleColumnAscAndDesc (line 10) | func TestSortSingleColumnAscAndDesc(t *testing.T) {
function TestSortSingleColumnIntsAsc (line 64) | func TestSortSingleColumnIntsAsc(t *testing.T) {
function TestSortTwoColumnsAscDescMix (line 100) | func TestSortTwoColumnsAscDescMix(t *testing.T) {
function TestGetSortedRows (line 149) | func TestGetSortedRows(t *testing.T) {
FILE: table/strlimit.go
function limitStr (line 10) | func limitStr(str string, maxLen int) string {
FILE: table/strlimit_test.go
function TestLimitStr (line 13) | func TestLimitStr(t *testing.T) {
FILE: table/update.go
method moveHighlightUp (line 8) | func (m *Model) moveHighlightUp() {
method moveHighlightDown (line 18) | func (m *Model) moveHighlightDown() {
method toggleSelect (line 28) | func (m *Model) toggleSelect() {
method updateFilterTextInput (line 54) | func (m Model) updateFilterTextInput(msg tea.Msg) (Model, tea.Cmd) {
method handleKeypress (line 72) | func (m *Model) handleKeypress(msg tea.KeyMsg) {
method Update (line 130) | func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
FILE: table/update_test.go
function TestUnfocusedDoesntMove (line 11) | func TestUnfocusedDoesntMove(t *testing.T) {
function TestPageKeysDoNothingWhenNoPages (line 38) | func TestPageKeysDoNothingWhenNoPages(t *testing.T) {
function TestFocusedMovesWhenMoveKeysPressedPaged (line 82) | func TestFocusedMovesWhenMoveKeysPressedPaged(t *testing.T) {
function TestFocusedMovesWithCustomKeyMap (line 166) | func TestFocusedMovesWithCustomKeyMap(t *testing.T) {
function TestSelectingRowWhenTableUnselectableDoesNothing (line 221) | func TestSelectingRowWhenTableUnselectableDoesNothing(t *testing.T) {
function TestSelectingRowToggles (line 245) | func TestSelectingRowToggles(t *testing.T) {
function TestFilterWithKeypresses (line 281) | func TestFilterWithKeypresses(t *testing.T) {
function TestSelectOnFilteredTableDoesntLoseRows (line 338) | func TestSelectOnFilteredTableDoesntLoseRows(t *testing.T) {
FILE: table/view.go
method View (line 13) | func (m Model) View() string {
FILE: table/view_selectable_test.go
function TestSimple3x3WithSelectableDefaults (line 10) | func TestSimple3x3WithSelectableDefaults(t *testing.T) {
function TestSimple3x3WithCustomSelectableText (line 46) | func TestSimple3x3WithCustomSelectableText(t *testing.T) {
function TestSimple3x3WithCustomSelectableTextAndFooter (line 84) | func TestSimple3x3WithCustomSelectableTextAndFooter(t *testing.T) {
function TestRegeneratingColumnsKeepsSelectableText (line 125) | func TestRegeneratingColumnsKeepsSelectableText(t *testing.T) {
FILE: table/view_test.go
function TestBasicTableShowsAllHeaders (line 14) | func TestBasicTableShowsAllHeaders(t *testing.T) {
function TestBasicTableTruncatesLongHeaders (line 40) | func TestBasicTableTruncatesLongHeaders(t *testing.T) {
function TestNilColumnsSafelyReturnsEmptyView (line 66) | func TestNilColumnsSafelyReturnsEmptyView(t *testing.T) {
function TestSingleCellView (line 72) | func TestSingleCellView(t *testing.T) {
function TestSingleColumnView (line 86) | func TestSingleColumnView(t *testing.T) {
function TestSingleColumnViewSorted (line 106) | func TestSingleColumnViewSorted(t *testing.T) {
function TestSingleRowView (line 126) | func TestSingleRowView(t *testing.T) {
function TestSingleRowViewWithHiddenHeader (line 142) | func TestSingleRowViewWithHiddenHeader(t *testing.T) {
function TestTableWithNoRowsAndHiddenHeaderHidesTable (line 162) | func TestTableWithNoRowsAndHiddenHeaderHidesTable(t *testing.T) {
function TestSimple3x3 (line 176) | func TestSimple3x3(t *testing.T) {
function TestSimple3x3WithHiddenHeader (line 212) | func TestSimple3x3WithHiddenHeader(t *testing.T) {
function TestSingleHeaderWithFooter (line 246) | func TestSingleHeaderWithFooter(t *testing.T) {
function TestSingleColumnWithFooterAndHiddenHeader (line 261) | func TestSingleColumnWithFooterAndHiddenHeader(t *testing.T) {
function TestSingleRowWithFooterView (line 276) | func TestSingleRowWithFooterView(t *testing.T) {
function TestSingleRowWithFooterViewAndBaseStyle (line 294) | func TestSingleRowWithFooterViewAndBaseStyle(t *testing.T) {
function TestSingleRowWithFooterViewAndBaseStyleWithHiddenHeader (line 312) | func TestSingleRowWithFooterViewAndBaseStyleWithHiddenHeader(t *testing....
function TestSingleColumnWithFooterView (line 331) | func TestSingleColumnWithFooterView(t *testing.T) {
function TestSingleColumnWithFooterViewAndHiddenHeader (line 353) | func TestSingleColumnWithFooterViewAndHiddenHeader(t *testing.T) {
function TestSimple3x3WithFooterView (line 376) | func TestSimple3x3WithFooterView(t *testing.T) {
function TestSimple3x3WithMissingData (line 414) | func TestSimple3x3WithMissingData(t *testing.T) {
function TestFmtStringWithMissingData (line 457) | func TestFmtStringWithMissingData(t *testing.T) {
function TestSimple3x3WithMissingIndicator (line 500) | func TestSimple3x3WithMissingIndicator(t *testing.T) {
function TestFmtStringWithMissingIndicator (line 550) | func TestFmtStringWithMissingIndicator(t *testing.T) {
function TestSimple3x3WithMissingIndicatorStyled (line 593) | func TestSimple3x3WithMissingIndicatorStyled(t *testing.T) {
function TestFmtStringWithMissingIndicatorStyled (line 639) | func TestFmtStringWithMissingIndicatorStyled(t *testing.T) {
function TestPaged3x3WithNoSpecifiedFooter (line 685) | func TestPaged3x3WithNoSpecifiedFooter(t *testing.T) {
function TestPaged3x3WithStaticFooter (line 722) | func TestPaged3x3WithStaticFooter(t *testing.T) {
function TestSimple3x3StyleOverridesAsBaseColumnRowCell (line 759) | func TestSimple3x3StyleOverridesAsBaseColumnRowCell(t *testing.T) {
function TestSimple3x3CellStyleFuncOverridesAsBaseColumnRowCell (line 801) | func TestSimple3x3CellStyleFuncOverridesAsBaseColumnRowCell(t *testing.T) {
function TestRowStyleFuncAppliesAfterBaseStyleAndColStylesAndBeforeRowStyle (line 854) | func TestRowStyleFuncAppliesAfterBaseStyleAndColStylesAndBeforeRowStyle(...
function TestRowStyleFuncAppliesHighlighted (line 905) | func TestRowStyleFuncAppliesHighlighted(t *testing.T) {
function Test3x3WithFilterFooter (line 961) | func Test3x3WithFilterFooter(t *testing.T) {
function TestSingleCellFlexView (line 1031) | func TestSingleCellFlexView(t *testing.T) {
function TestSimpleFlex3x3 (line 1045) | func TestSimpleFlex3x3(t *testing.T) {
function TestSimpleFlex3x3AtAllTargetWidths (line 1081) | func TestSimpleFlex3x3AtAllTargetWidths(t *testing.T) {
function TestViewResizesWhenColumnsChange (line 1120) | func TestViewResizesWhenColumnsChange(t *testing.T) {
function TestMaxWidthHidesOverflow (line 1157) | func TestMaxWidthHidesOverflow(t *testing.T) {
function TestMaxWidthHasNoEffectForExactFit (line 1189) | func TestMaxWidthHasNoEffectForExactFit(t *testing.T) {
function TestMaxWidthHasNoEffectForSmaller (line 1228) | func TestMaxWidthHasNoEffectForSmaller(t *testing.T) {
function TestMaxWidthHidesOverflowWithSingleCharExtra (line 1267) | func TestMaxWidthHidesOverflowWithSingleCharExtra(t *testing.T) {
function TestMaxWidthHidesOverflowWithTwoCharExtra (line 1299) | func TestMaxWidthHidesOverflowWithTwoCharExtra(t *testing.T) {
function TestScrolledTableSizesFooterCorrectly (line 1339) | func TestScrolledTableSizesFooterCorrectly(t *testing.T) {
function TestHorizontalScrollCaretIsRightAligned (line 1370) | func TestHorizontalScrollCaretIsRightAligned(t *testing.T) {
function Test3x3WithRoundedBorder (line 1403) | func Test3x3WithRoundedBorder(t *testing.T) {
function TestSingleColumnViewSortedAndFormatted (line 1441) | func TestSingleColumnViewSortedAndFormatted(t *testing.T) {
function TestMinimumHeightSingleCellView (line 1463) | func TestMinimumHeightSingleCellView(t *testing.T) {
function TestMinimumHeightSingleColumnView (line 1479) | func TestMinimumHeightSingleColumnView(t *testing.T) {
function TestMinimumHeightHeaderNoData (line 1501) | func TestMinimumHeightHeaderNoData(t *testing.T) {
function TestMinimumHeightSingleRowWithHiddenHeader (line 1519) | func TestMinimumHeightSingleRowWithHiddenHeader(t *testing.T) {
function TestMinimumHeightNoRowsAndHiddenHeader (line 1541) | func TestMinimumHeightNoRowsAndHiddenHeader(t *testing.T) {
function TestMinimumHeightSingleColumnNoDataWithFooter (line 1557) | func TestMinimumHeightSingleColumnNoDataWithFooter(t *testing.T) {
function TestMinimumHeightSingleColumnWithFooterAndHiddenHeader (line 1574) | func TestMinimumHeightSingleColumnWithFooterAndHiddenHeader(t *testing.T) {
function TestMinimumHeightSingleRowWithFooter (line 1593) | func TestMinimumHeightSingleRowWithFooter(t *testing.T) {
function TestMinimumHeightSingleColumnWithFooter (line 1613) | func TestMinimumHeightSingleColumnWithFooter(t *testing.T) {
function TestMinimumHeightExtraRow (line 1636) | func TestMinimumHeightExtraRow(t *testing.T) {
function TestMinimumHeightSmallerThanTable (line 1654) | func TestMinimumHeightSmallerThanTable(t *testing.T) {
function TestMultilineEnabled (line 1676) | func TestMultilineEnabled(t *testing.T) {
function TestMultilineDisabledByDefault (line 1703) | func TestMultilineDisabledByDefault(t *testing.T) {
function TestMultilineDisabledExplicite (line 1726) | func TestMultilineDisabledExplicite(t *testing.T) {
Condensed preview — 86 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (316K chars).
[
{
"path": ".editorconfig",
"chars": 212,
"preview": "root = true\n\n[*]\nindent_size = 2\nindent_style = space\ninsert_final_newline = true\ntrim_trailing_whitespace = true\nend_of"
},
{
"path": ".github/workflows/build.yml",
"chars": 621,
"preview": "name: build\non:\n push:\n tags:\n - v*\n branches:\n - main\n pull_request:\n\njobs:\n buildandtest:\n name:"
},
{
"path": ".github/workflows/codeql-analysis.yml",
"chars": 1117,
"preview": "\nname: \"CodeQL\"\n\non:\n push:\n branches: [ main ]\n pull_request:\n # The branches below must be a subset of the bra"
},
{
"path": ".github/workflows/coverage.yml",
"chars": 798,
"preview": "name: coverage\non:\n push:\n tags:\n - v*\n branches:\n - main\n pull_request:\n\njobs:\n coverage:\n name: "
},
{
"path": ".github/workflows/lint.yml",
"chars": 532,
"preview": "name: golangci-lint\non:\n push:\n tags:\n - v*\n branches:\n - main\n pull_request:\npermissions:\n contents:"
},
{
"path": ".gitignore",
"chars": 386,
"preview": "# Binaries for programs and plugins\n*.exe\n*.exe~\n*.dll\n*.so\n*.dylib\n/bin\n\n# Test binary, built with `go test -c`\n*.test\n"
},
{
"path": ".go-version",
"chars": 7,
"preview": "1.17.6\n"
},
{
"path": ".golangci.yaml",
"chars": 1370,
"preview": "version: \"2\"\nlinters:\n enable:\n - asciicheck\n - bidichk\n - bodyclose\n - contextcheck\n - cyclop\n - dep"
},
{
"path": "CONTRIBUTING.md",
"chars": 1827,
"preview": "# Contributing\n\nThanks for your interest in contributing!\n\n## Contributing issues\n\nPlease feel free to open an issue if "
},
{
"path": "LICENSE",
"chars": 1074,
"preview": "MIT License\n\nCopyright (c) 2022 Brandon Fulljames\n\nPermission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "Makefile",
"chars": 1881,
"preview": "ifeq ($(OS), Windows_NT)\n\tEXE_EXT=.exe\nelse\n\tEXE_EXT=\nendif\n\n.PHONY: example-pokemon\nexample-pokemon:\n\t@go run ./example"
},
{
"path": "README.md",
"chars": 8088,
"preview": "# Bubble-table\n\n<p>\n <a href=\"https://github.com/Evertras/bubble-table/releases\"><img src=\"https://img.shields.io/githu"
},
{
"path": "examples/dimensions/README.md",
"chars": 202,
"preview": "# Dimensions\n\nShows some simple tables with various dimensions.\n\n<img width=\"534\" alt=\"image\" src=\"https://user-images.g"
},
{
"path": "examples/dimensions/main.go",
"chars": 2445,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"strings\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/charmbracelet/li"
},
{
"path": "examples/events/README.md",
"chars": 337,
"preview": "# Events example\n\nThis example shows how to use events to handle triggers for data retrieval or\nother desired behavior. "
},
{
"path": "examples/events/main.go",
"chars": 5006,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"time\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/charmbracelet/lipgl"
},
{
"path": "examples/features/README.md",
"chars": 416,
"preview": "# Full feature example\n\nThis table contains most of the implemented features, but as more features have\nbeen added some "
},
{
"path": "examples/features/main.go",
"chars": 5006,
"preview": "// This file contains a full demo of most available features, for both testing\n// and for reference\npackage main\n\nimport"
},
{
"path": "examples/filter/README.md",
"chars": 222,
"preview": "# Filter example\n\nShows how the table can use a built-in filter to filter results.\n\n<img width=\"1052\" alt=\"image\" src=\"h"
},
{
"path": "examples/filter/main.go",
"chars": 2705,
"preview": "package main\n\nimport (\n\t\"log\"\n\t\"strings\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/evertras/bubble-table/t"
},
{
"path": "examples/filterapi/README.md",
"chars": 369,
"preview": "# Filter API example\n\nSimilar to the [regular filter example](../filter/), but uses an external text\nbox control for fil"
},
{
"path": "examples/filterapi/main.go",
"chars": 3344,
"preview": "package main\n\nimport (\n\t\"github.com/charmbracelet/bubbles/textinput\"\n\t\"log\"\n\t\"strings\"\n\n\ttea \"github.com/charmbracelet/b"
},
{
"path": "examples/flex/README.md",
"chars": 293,
"preview": "# Flex example\n\nThis example shows how to use flexible-width columns. The example stretches to\nfill the full width of t"
},
{
"path": "examples/flex/main.go",
"chars": 3252,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/charmbracelet/lipgloss\"\n\t\"g"
},
{
"path": "examples/metadata/README.md",
"chars": 371,
"preview": "# Metadata example\n\nThis is the [pokemon example](../pokemon) with metadata attached to the rows in\norder to retrieve da"
},
{
"path": "examples/metadata/main.go",
"chars": 4731,
"preview": "// This is a more data-driven example of the Pokemon table\npackage main\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\n\ttea \"github.com/charmb"
},
{
"path": "examples/multiline/README.md",
"chars": 493,
"preview": "# Multiline Example\n\nThis example code showcases the implementation of a multiline feature. The feature enables users t"
},
{
"path": "examples/multiline/main.go",
"chars": 3580,
"preview": "package main\n\nimport (\n\t\"log\"\n\t\"strings\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/charmbracelet/lipgloss\""
},
{
"path": "examples/pagination/README.md",
"chars": 333,
"preview": "# Pagination example\n\nThis example shows how to paginate data that would be too long to show in a\nsingle screen. It als"
},
{
"path": "examples/pagination/main.go",
"chars": 4121,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"math/rand\"\n\t\"strings\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/cha"
},
{
"path": "examples/pokemon/README.md",
"chars": 285,
"preview": "# Pokemon example\n\nThis example is a general use of the table using various features to nicely\ndisplay a table of Pokemo"
},
{
"path": "examples/pokemon/main.go",
"chars": 4493,
"preview": "package main\n\nimport (\n\t\"log\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/charmbracelet/lipgloss\"\n\t\"github.c"
},
{
"path": "examples/scrolling/README.md",
"chars": 278,
"preview": "# Scrolling example\n\nThis example shows how to use scrolling to navigate particularly large tables\nthat may not fit nice"
},
{
"path": "examples/scrolling/main.go",
"chars": 1815,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"strings\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/evertras/bubble-"
},
{
"path": "examples/simplest/README.md",
"chars": 335,
"preview": "# Simplest example\n\nThis is a bare bones example of how to get started with the table component. It\nuses all the defaul"
},
{
"path": "examples/simplest/main.go",
"chars": 1398,
"preview": "package main\n\nimport (\n\t\"log\"\n\t\"strings\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/evertras/bubble-table/t"
},
{
"path": "examples/sorting/README.md",
"chars": 429,
"preview": "# Sorting example\n\nThis example shows how to use the sorting feature, which sorts columns. It\ndemonstrates how numbers "
},
{
"path": "examples/sorting/main.go",
"chars": 2562,
"preview": "package main\n\nimport (\n\t\"log\"\n\t\"strings\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/evertras/bubble-table/t"
},
{
"path": "examples/updates/README.md",
"chars": 209,
"preview": "# Update example\n\nShows how to update data in the table from an external API returning data.\n\n\n\nvar benchView string\n\nfunc benchTable(numColumns, numDataRows int) Model {\n"
},
{
"path": "table/border.go",
"chars": 10038,
"preview": "package table\n\nimport \"github.com/charmbracelet/lipgloss\"\n\n// Border defines the borders in and around the table.\ntype B"
},
{
"path": "table/calc.go",
"chars": 487,
"preview": "package table\n\n// Keep compatibility with Go 1.21 by re-declaring min.\n//\n//nolint:predeclared\nfunc min(x, y int) int {\n"
},
{
"path": "table/calc_test.go",
"chars": 1769,
"preview": "package table\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\n// A bit overkill but let's be thoro"
},
{
"path": "table/cell.go",
"chars": 2046,
"preview": "package table\n\nimport \"github.com/charmbracelet/lipgloss\"\n\n// StyledCell represents a cell in the table that has a parti"
},
{
"path": "table/column.go",
"chars": 2893,
"preview": "package table\n\nimport (\n\t\"github.com/charmbracelet/lipgloss\"\n)\n\n// Column is a column in the table.\ntype Column struct {"
},
{
"path": "table/column_test.go",
"chars": 4220,
"preview": "package table\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/charmbracelet/lipgloss\"\n\t\"github.com/stretchr/testify/assert\"\n)\n"
},
{
"path": "table/data.go",
"chars": 989,
"preview": "package table\n\nimport \"time\"\n\n// This is just a bunch of data type checks, so... no linting here\n//\n//nolint:cyclop\nfunc"
},
{
"path": "table/data_test.go",
"chars": 955,
"preview": "package table\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestAsInt(t *testing.T) {\n\tche"
},
{
"path": "table/dimensions.go",
"chars": 2617,
"preview": "package table\n\nimport (\n\t\"github.com/charmbracelet/lipgloss\"\n)\n\nfunc (m *Model) recalculateWidth() {\n\tif m.targetTotalWi"
},
{
"path": "table/dimensions_test.go",
"chars": 5192,
"preview": "package table\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\n// This function is only long becaus"
},
{
"path": "table/doc.go",
"chars": 1053,
"preview": "/*\nPackage table contains a Bubble Tea component for an interactive and customizable\ntable.\n\nThe simplest useful table c"
},
{
"path": "table/events.go",
"chars": 2156,
"preview": "package table\n\n// UserEvent is some state change that has occurred due to user input. These will\n// ONLY be generated w"
},
{
"path": "table/events_test.go",
"chars": 5785,
"preview": "package table\n\nimport (\n\t\"testing\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nf"
},
{
"path": "table/filter.go",
"chars": 3670,
"preview": "package table\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\n// FilterFuncInput is the input to a FilterFunc. It's a struct so we can ad"
},
{
"path": "table/filter_test.go",
"chars": 14202,
"preview": "package table\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/charmbracelet/bubbles/textinput\"\n\ttea \"github.com/charmb"
},
{
"path": "table/footer.go",
"chars": 1292,
"preview": "package table\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\nfunc (m Model) hasFooter() bool {\n\treturn m.footerVisible && (m.staticFoote"
},
{
"path": "table/header.go",
"chars": 2456,
"preview": "package table\n\nimport \"github.com/charmbracelet/lipgloss\"\n\n// This is long and could use some refactoring in the future,"
},
{
"path": "table/keys.go",
"chars": 3363,
"preview": "package table\n\nimport \"github.com/charmbracelet/bubbles/key\"\n\n// KeyMap defines the keybindings for the table when it's "
},
{
"path": "table/keys_test.go",
"chars": 2737,
"preview": "package table\n\nimport (\n\t\"testing\"\n\n\t\"github.com/charmbracelet/bubbles/help\"\n\t\"github.com/charmbracelet/bubbles/key\"\n\t\"g"
},
{
"path": "table/model.go",
"chars": 3720,
"preview": "package table\n\nimport (\n\t\"github.com/charmbracelet/bubbles/key\"\n\t\"github.com/charmbracelet/bubbles/textinput\"\n\ttea \"gith"
},
{
"path": "table/model_test.go",
"chars": 186,
"preview": "package table\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestModelInitReturnsNil(t *testing.T) "
},
{
"path": "table/options.go",
"chars": 13336,
"preview": "package table\n\nimport (\n\t\"github.com/charmbracelet/bubbles/key\"\n\t\"github.com/charmbracelet/bubbles/textinput\"\n\t\"github.c"
},
{
"path": "table/options_test.go",
"chars": 6215,
"preview": "package table\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestWithHighlightedRowSet(t *testing.T"
},
{
"path": "table/overflow.go",
"chars": 487,
"preview": "package table\n\nimport \"github.com/charmbracelet/lipgloss\"\n\nconst columnKeyOverflowRight = \"___overflow_r___\"\nconst colum"
},
{
"path": "table/pagination.go",
"chars": 2222,
"preview": "package table\n\n// PageSize returns the current page size for the table, or 0 if there is no\n// pagination enabled.\nfunc "
},
{
"path": "table/pagination_test.go",
"chars": 7552,
"preview": "package table\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc genPaginationTable(count, pageSize int"
},
{
"path": "table/query.go",
"chars": 3053,
"preview": "package table\n\n// GetColumnSorting returns the current sorting rules for the table as a list of\n// SortColumns, which ar"
},
{
"path": "table/query_test.go",
"chars": 5538,
"preview": "package table\n\nimport (\n\t\"testing\"\n\n\t\"github.com/charmbracelet/bubbles/textinput\"\n\ttea \"github.com/charmbracelet/bubblet"
},
{
"path": "table/row.go",
"chars": 6363,
"preview": "package table\n\nimport (\n\t\"fmt\"\n\t\"sync/atomic\"\n\n\t\"github.com/charmbracelet/lipgloss\"\n\t\"github.com/muesli/reflow/wordwrap\""
},
{
"path": "table/scrolling.go",
"chars": 1141,
"preview": "package table\n\nfunc (m *Model) scrollRight() {\n\tif m.horizontalScrollOffsetCol < m.maxHorizontalColumnIndex {\n\t\tm.horizo"
},
{
"path": "table/scrolling_fuzz_test.go",
"chars": 1851,
"preview": "//go:build go1.18\n// +build go1.18\n\npackage table\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\ttea \"github.com/charmbracelet"
},
{
"path": "table/scrolling_test.go",
"chars": 9105,
"preview": "package table\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/charmbracelet/bubbles/key\"\n\ttea \"github.com/charmbracelet/bubble"
},
{
"path": "table/sort.go",
"chars": 3970,
"preview": "package table\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n)\n\n// SortDirection indicates whether a column should sort by ascending or descen"
},
{
"path": "table/sort_test.go",
"chars": 3977,
"preview": "package table\n\nimport (\n\t\"testing\"\n\n\t\"github.com/charmbracelet/lipgloss\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc T"
},
{
"path": "table/strlimit.go",
"chars": 430,
"preview": "package table\n\nimport (\n\t\"strings\"\n\n\t\"github.com/muesli/reflow/ansi\"\n\t\"github.com/muesli/reflow/truncate\"\n)\n\nfunc limitS"
},
{
"path": "table/strlimit_test.go",
"chars": 2060,
"preview": "package table\n\nimport (\n\t\"testing\"\n\n\t\"github.com/muesli/reflow/ansi\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\n// This fu"
},
{
"path": "table/update.go",
"chars": 3007,
"preview": "package table\n\nimport (\n\t\"github.com/charmbracelet/bubbles/key\"\n\ttea \"github.com/charmbracelet/bubbletea\"\n)\n\nfunc (m *Mo"
},
{
"path": "table/update_test.go",
"chars": 10166,
"preview": "package table\n\nimport (\n\t\"testing\"\n\n\t\"github.com/charmbracelet/bubbles/key\"\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\""
},
{
"path": "table/view.go",
"chars": 1406,
"preview": "package table\n\nimport (\n\t\"strings\"\n\n\t\"github.com/charmbracelet/lipgloss\"\n)\n\n// View renders the table. It does not end i"
},
{
"path": "table/view_selectable_test.go",
"chars": 3309,
"preview": "package table\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestSimple3x3WithSelectableDefa"
},
{
"path": "table/view_test.go",
"chars": 36491,
"preview": "package table\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/charmbracel"
}
]
About this extraction
This page contains the full source code of the Evertras/bubble-table GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 86 files (267.0 KB), approximately 83.9k tokens, and a symbol index with 520 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.