Full Code of Evertras/bubble-table for AI

main ef37988ad03a cached
86 files
267.0 KB
83.9k tokens
520 symbols
1 requests
Download .txt
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).

![Styled table](https://user-images.githubusercontent.com/5923958/188168029-0de392c8-dbb0-47da-93a0-d2a6e3d46838.png)

[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.

![Styled table](https://user-images.githubusercontent.com/5923958/156778142-cc1a32e1-1b1e-4a65-b699-187f39f0f946.png)


================================================
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.

![Styled table](https://user-images.githubusercontent.com/5923958/156778142-cc1a32e1-1b1e-4a65-b699-187f39f0f946.png)


================================================
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.

![table-gif](https://user-images.githubusercontent.com/5923958/170802479-a4395407-f286-42d6-9165-0580db6030db.gif)


================================================
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
Download .txt
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
Download .txt
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![table-gif](https://user-"
  },
  {
    "path": "examples/updates/data.go",
    "chars": 702,
    "preview": "package main\n\nimport \"math/rand\"\n\n// SomeData represent some real data of some sort, unaware of tables\ntype SomeData str"
  },
  {
    "path": "examples/updates/main.go",
    "chars": 4053,
    "preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"strings\"\n\t\"time\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/charmbra"
  },
  {
    "path": "flake.nix",
    "chars": 433,
    "preview": "{\n  inputs = {\n    nixpkgs.url = \"nixpkgs/nixos-23.11\";\n    flake-utils.url = \"github:numtide/flake-utils\";\n  };\n\n  outp"
  },
  {
    "path": "go.mod",
    "chars": 1047,
    "preview": "module github.com/evertras/bubble-table\n\ngo 1.18\n\nrequire (\n\tgithub.com/charmbracelet/bubbles v0.11.0\n\tgithub.com/charmb"
  },
  {
    "path": "go.sum",
    "chars": 5057,
    "preview": "github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=\ngithub.com/atotto/clipboard v0.1.4/go"
  },
  {
    "path": "table/benchmarks_test.go",
    "chars": 1682,
    "preview": "package table\n\nimport (\n\t\"fmt\"\n\t\"testing\"\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.

Copied to clipboard!