[
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\nindent_size = 2\nindent_style = space\ninsert_final_newline = true\ntrim_trailing_whitespace = true\nend_of_line = lf\n\n[{*.go,go.*}]\nindent_size = 4\nindent_style = tab\n\n[Makefile]\nindent_style = tab\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: build\non:\n  push:\n    tags:\n      - v*\n    branches:\n      - main\n  pull_request:\n\njobs:\n  buildandtest:\n    name: Build and test\n    strategy:\n      matrix:\n        go-version: [~1.18, ^1]\n        os: [ubuntu-latest]\n    runs-on: ${{ matrix.os }}\n    steps:\n      - name: Install Go\n        uses: actions/setup-go@v4\n        with:\n          go-version: ${{ matrix.go-version }}\n\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Download Go modules\n        run: go mod download\n\n      - name: Build\n        run: go build -v ./table\n\n      - name: Test\n        run: go test -race ./table\n"
  },
  {
    "path": ".github/workflows/codeql-analysis.yml",
    "content": "\nname: \"CodeQL\"\n\non:\n  push:\n    branches: [ main ]\n  pull_request:\n    # The branches below must be a subset of the branches above\n    branches: [ main ]\n  schedule:\n    - cron: '29 4 * * 4'\n\njobs:\n  analyze:\n    name: Analyze\n    runs-on: ubuntu-latest\n    permissions:\n      actions: read\n      contents: read\n      security-events: write\n\n    strategy:\n      fail-fast: false\n      matrix:\n        language: [ 'go' ]\n\n    steps:\n    - name: Checkout repository\n      uses: actions/checkout@v4\n\n    - name: Initialize CodeQL\n      uses: github/codeql-action/init@v2\n      with:\n        languages: ${{ matrix.language }}\n    - name: Autobuild\n      uses: github/codeql-action/autobuild@v2\n\n    # ℹ️ Command-line programs to run using the OS shell.\n    # 📚 https://git.io/JvXDl\n\n    # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines\n    #    and modify them (or add more) to build your code if your project\n    #    uses a compiled language\n\n    #- run: |\n    #   make bootstrap\n    #   make release\n\n    - name: Perform CodeQL Analysis\n      uses: github/codeql-action/analyze@v2\n"
  },
  {
    "path": ".github/workflows/coverage.yml",
    "content": "name: coverage\non:\n  push:\n    tags:\n      - v*\n    branches:\n      - main\n  pull_request:\n\njobs:\n  coverage:\n    name: Report Coverage\n    runs-on: ubuntu-latest\n    steps:\n      - name: Install Go\n        uses: actions/setup-go@v4\n        with:\n          go-version: \"1.18.10\"\n\n      - name: Check out code\n        uses: actions/checkout@v4\n\n      - name: Install deps\n        run: |\n          go mod download\n\n      - name: Run tests with coverage output\n        run: |\n          go test -race -covermode atomic -coverprofile=covprofile ./...\n\n      - name: Install goveralls\n        run: go install github.com/mattn/goveralls@latest\n\n      - name: Send coverage\n        env:\n          COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        run: goveralls -coverprofile=covprofile -service=github\n"
  },
  {
    "path": ".github/workflows/lint.yml",
    "content": "name: golangci-lint\non:\n  push:\n    tags:\n      - v*\n    branches:\n      - main\n  pull_request:\npermissions:\n  contents: read\njobs:\n  golangci:\n    name: lint\n    runs-on: ubuntu-latest\n    steps:\n      # For whatever reason, 1.21.9 blows up because it can't\n      # find 'max' in some go lib... pinning to 1.21.4 fixes this\n      - name: Install Go\n        uses: actions/setup-go@v4\n        with:\n          go-version: 1.21.4\n      - name: Checkout code\n        uses: actions/checkout@v4\n      - name: Lint\n        run: make lint\n\n"
  },
  {
    "path": ".gitignore",
    "content": "# 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\n# Profiling outputs\n*.prof\n\n# Outputs of the go coverage tool, specifically when used with LiteIDE\n*.out\n*.coverage\n\n# Go vendor folder\nvendor/\n\n# Mac stuff\n.DS_Store\n\n# Envrc with direnv\n.envrc\n\n# Sandbox area for experimenting without worrying about git\nsandbox\n\n"
  },
  {
    "path": ".go-version",
    "content": "1.17.6\n"
  },
  {
    "path": ".golangci.yaml",
    "content": "version: \"2\"\nlinters:\n  enable:\n    - asciicheck\n    - bidichk\n    - bodyclose\n    - contextcheck\n    - cyclop\n    - depguard\n    - dogsled\n    - dupl\n    - durationcheck\n    - err113\n    - errname\n    - errorlint\n    - exhaustive\n    - forbidigo\n    - forcetypeassert\n    - funlen\n    - gocognit\n    - goconst\n    - gocyclo\n    - godot\n    - goheader\n    - gomoddirectives\n    - gomodguard\n    - goprintffuncname\n    - gosec\n    - importas\n    - ireturn\n    - lll\n    - makezero\n    - misspell\n    - mnd\n    - nakedret\n    - nestif\n    - nilerr\n    - nilnil\n    - nlreturn\n    - noctx\n    - nolintlint\n    - prealloc\n    - predeclared\n    - promlinter\n    - revive\n    - rowserrcheck\n    - sqlclosecheck\n    - staticcheck\n    - tagliatelle\n    - thelper\n    - tparallel\n    - unconvert\n    - unparam\n    - varnamelen\n    - wastedassign\n    - whitespace\n    - wrapcheck\n  settings:\n    depguard:\n      rules:\n        main:\n          list-mode: lax\n          allow:\n            - github.com/stretchr/testify/assert\n            - github.com/charmbracelet/lipgloss\n            - github.com/muesli/reflow/wordwrap\n  exclusions:\n    generated: lax\n    paths:\n      - third_party$\n      - builtin$\n      - examples$\nformatters:\n  enable:\n    - gci\n    - gofmt\n    - goimports\n  exclusions:\n    generated: lax\n    paths:\n      - third_party$\n      - builtin$\n      - examples$\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing\n\nThanks for your interest in contributing!\n\n## Contributing issues\n\nPlease feel free to open an issue if you think something is working incorrectly,\nif you have a feature request, or if you just have any questions.  No templates\nare in place, all I ask is that you provide relevant information if you believe\nsomething is working incorrectly so we can sort it out quickly.\n\n## Contributing code\n\nAll contributions should have an associated issue.  If you are at all unsure\nabout how to solve the issue, please ask!  I'd rather chat about possible\nsolutions than have someone spend hours on a PR that requires a lot of major\nchanges.\n\nTest coverage is important.  If you end up with a small (<1%) drop, I'm happy to\nhelp cover the gap, but generally any new features or changes should have some\ntests as well.  If you're not sure how to test something, feel free to ask!\n\nLinting can be done with `make lint`.  Running `fmt` can be done with `make fmt`.\nTests can be run with `make test`.\n\nDoing all these before submitting the PR can help you pass the existing\ngatekeeping tests without having to wait for them to run every time.\n\nThe name of the PR, commit messages, and branch names aren't very important,\nthere are no special triggers or filters or anything in place that depend on names.\nThe name of the PR should at least reasonably describe the change, because this\nis what people will see in the commit history and what goes into the change logs.\n\nExported functions should generally follow the pattern of returning a `Model`\nand not use a pointer receiver.  This matches more closely with the flow of Bubble\nTea (and Elm), and discourages users from making mutable changes with unintended\nside effects.  Unexported functions are free to use pointer receivers for both\nsimplicity and performance reasons.\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2022 Brandon Fulljames\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": "ifeq ($(OS), Windows_NT)\n\tEXE_EXT=.exe\nelse\n\tEXE_EXT=\nendif\n\n.PHONY: example-pokemon\nexample-pokemon:\n\t@go run ./examples/pokemon/*.go\n\n.PHONY: example-metadata\nexample-metadata:\n\t@go run ./examples/metadata/*.go\n\n.PHONY: example-dimensions\nexample-dimensions:\n\t@go run ./examples/dimensions/main.go\n\n.PHONY: example-events\nexample-events:\n\t@go run ./examples/events/main.go\n\n.PHONY: example-features\nexample-features:\n\t@go run ./examples/features/main.go\n\n.PHONY: example-multiline\nexample-multiline:\n\t@go run ./examples/multiline/main.go\n\n.PHONY: example-filter\nexample-filter:\n\t@go run ./examples/filter/*.go\n\n.PHONY: example-filterapi\nexample-filterapi:\n\t@go run ./examples/filterapi/*.go\n\n.PHONY: example-flex\nexample-flex:\n\t@go run ./examples/flex/*.go\n\n.PHONY: example-pagination\nexample-pagination:\n\t@go run ./examples/pagination/*.go\n\n.PHONY: example-simplest\nexample-simplest:\n\t@go run ./examples/simplest/*.go\n\n.PHONY: example-scrolling\nexample-scrolling:\n\t@go run ./examples/scrolling/*.go\n\n.PHONY: example-sorting\nexample-sorting:\n\t@go run ./examples/sorting/*.go\n\n.PHONY: example-updates\nexample-updates:\n\t@go run ./examples/updates/*.go\n\n.PHONY: test\ntest:\n\t@go test -race -cover ./table\n\n.PHONY: test-coverage\ntest-coverage: coverage.out\n\t@go tool cover -html=coverage.out\n\n.PHONY: benchmark\nbenchmark:\n\t@go test -run=XXX -bench=. -benchmem ./table\n\n.PHONY: lint\nlint: ./bin/golangci-lint$(EXE_EXT)\n\t@./bin/golangci-lint$(EXE_EXT) run ./table\n\ncoverage.out: table/*.go go.*\n\t@go test -coverprofile=coverage.out ./table\n\n.PHONY: fmt\nfmt: ./bin/gci$(EXE_EXT)\n\t@go fmt ./...\n\t@./bin/gci$(EXE_EXT) write --skip-generated ./table/*.go\n\n./bin/golangci-lint$(EXE_EXT):\n\tcurl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b ./bin v2.3.1\n\n./bin/gci$(EXE_EXT):\n\tGOBIN=$(shell pwd)/bin go install github.com/daixiang0/gci@v0.9.1\n"
  },
  {
    "path": "README.md",
    "content": "# Bubble-table\n\n<p>\n  <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>\n  <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>\n  <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>\n  <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>\n</p>\n\nA customizable, interactive table component for the\n[Bubble Tea framework](https://github.com/charmbracelet/bubbletea).\n\n![Styled table](https://user-images.githubusercontent.com/5923958/188168029-0de392c8-dbb0-47da-93a0-d2a6e3d46838.png)\n\n[View above sample source code](./examples/pokemon)\n\n## Contributing\n\nContributions welcome, please [check the contributions doc](./CONTRIBUTING.md)\nfor a few helpful tips!\n\n## Features\n\nFor a code reference of most available features, please see the [full feature example](./examples/features).\nIf you want to get started with a simple default table, [check the simplest example](./examples/simplest).\n\nDisplays a table with a header, rows, footer, and borders.  The header can be\nhidden, and the footer can be set to automatically show page information, use\ncustom text, or be hidden by default.\n\nColumns can be fixed-width [or flexible width](./examples/flex).  A maximum\nwidth can be specified which enables [horizontal scrolling](./examples/scrolling),\nand left-most columns can be frozen for easier reference.\n\nBorder shape is customizable with a basic thick square default.  The color can\nbe modified by applying a base style with `lipgloss.NewStyle().BorderForeground(...)`.\n\nStyles can be applied globally and to columns, rows, and individual cells.\nThe base style is applied first, then column, then row, then cell when\ndetermining overrides.  The default base style is a basic right-alignment.\n[See the main feature example](./examples/features) to see styles and\nhow they override each other.\n\nStyles can also be applied via a style function which can be used to apply\nzebra striping, data-specific formatting, etc.\n\nCan be focused to highlight a row and navigate with up/down (and j/k).  These\nkeys can be customized with a KeyMap.\n\nCan make rows selectable, and fetch the current selections.\n\nEvents can be checked for user interactions.\n\nPagination can be set with a given page size, which automatically generates a\nsimple footer to show the current page and total pages.\n\nBuilt-in filtering can be enabled by setting any columns as filterable, using\na text box in the footer and `/` (customizable by keybind) to start filtering.\n\nA missing indicator can be supplied to show missing data in rows.\n\nColumns can be sorted in either ascending or descending order.  Multiple columns\ncan be specified in a row.  If multiple columns are specified, first the table\nis sorted by the first specified column, then each group within that column is\nsorted in smaller and smaller groups.  [See the sorting example](examples/sorting)\nfor more information.  If a column contains numbers (either ints or floats),\nthe numbers will be sorted by numeric value.  Otherwise rendered string values\nwill be compared.\n\nIf a feature is confusing to use or could use a better example, please feel free\nto open an issue.\n\n## Defining table data\n\nA table is defined by a list of `Column` values that define the columns in the\ntable.  Each `Column` is associated with a unique string key.\n\nA table contains a list of `Row`s.  Each `Row` contains a `RowData` object which\nis simply a map of string column IDs to arbitrary `any` data values.\nWhen the table is rendered, each `Row` is checked for each `Column` key.  If the\nkey exists in the `Row`'s `RowData`, it is rendered with `fmt.Sprintf(\"%v\")`.\nIf it does not exist, nothing is rendered.\n\nExtra data in the `RowData` object is ignored.  This can be helpful to simply\ndump data into `RowData` and create columns that select what is interesting to\nview, or to generate different columns based on view options on the fly (see the\n[metadata example](./examples/metadata) for an example of using this).\n\nAn example is given below.  For more detailed examples, see\n[the examples directory](./examples).\n\n```golang\n// This makes it easier/safer to match against values, but isn't necessary\nconst (\n  // This value isn't visible anywhere, so a simple lowercase is fine\n  columnKeyID = \"id\"\n\n  // It's just a string, so it can be whatever, really!  They only must be unique\n  columnKeyName = \"何?!\"\n)\n\n// Note that there's nothing special about \"ID\" or \"Name\", these are completely\n// arbitrary columns\ncolumns := []table.Column{\n  table.NewColumn(columnKeyID, \"ID\", 5),\n  table.NewColumn(columnKeyName, \"Name\", 10),\n}\n\nrows := []table.Row{\n  // This row contains both an ID and a name\n  table.NewRow(table.RowData{\n    columnKeyID:          \"abc\",\n    columnKeyName:        \"Hello\",\n  }),\n\n  table.NewRow(table.RowData{\n    columnKeyID:          \"123\",\n    columnKeyName:        \"Oh no\",\n    // This field exists in the row data but won't be visible\n    \"somethingelse\": \"Super bold!\",\n  }),\n\n  table.NewRow(table.RowData{\n    columnKeyID:          \"def\",\n    // This row is missing the Name column, so it will use the supplied missing\n    // indicator if supplied when creating the table using the following option:\n    // .WithMissingDataIndicator(\"<ない>\") (or .WithMissingDataIndicatorStyled!)\n  }),\n\n  // We can also apply styling to the row or to individual cells\n\n  // This row has individual styling to make it bold\n  table.NewRow(table.RowData{\n    columnKeyID:          \"bold\",\n    columnKeyName:        \"Bolded\",\n  }).WithStyle(lipgloss.NewStyle().Bold(true).  ,\n\n  // This row also has individual styling to make it bold\n  table.NewRow(table.RowData{\n    columnKeyID:          \"alert\",\n    // This cell has styling applied on top of the bold\n    columnKeyName:        table.NewStyledCell(\"Alert\", lipgloss.NewStyle().Foreground(lipgloss.Color(\"#f88\"))),\n  }).WithStyle(lipgloss.NewStyle().Bold(true),\n}\n```\n\n### A note on 'metadata'\n\nThere may be cases where you wish to reference some kind of data object in the\ntable.  For example, a table of users may display a user name, ID, etc., and you\nmay wish to retrieve data about the user when the row is selected.  This can be\naccomplished by attaching hidden 'metadata' to the row in the same way as any\nother data.\n\n```golang\nconst (\n  columnKeyID = \"id\"\n  columnKeyName = \"名前\"\n  columnKeyUserData = \"userstuff\"\n)\n\n// Notice there is no \"userstuff\" column, so it won't be displayed\ncolumns := []table.Column{\n  table.NewColumn(columnKeyID, \"ID\", 5),\n  table.NewColumn(columnKeyName, \"Name\", 10),\n}\n\n// Just one user for this quick snippet, check the example for more\nuser := &SomeUser{\n  ID:   3,\n  Name: \"Evertras\",\n}\n\nrows := []table.Row{\n  // This row contains both an ID and a name\n  table.NewRow(table.RowData{\n    columnKeyID:       user.ID,\n    columnKeyName:     user.Name,\n\n    // This isn't displayed, but it remains attached to the row\n    columnKeyUserData: user,\n  }),\n}\n```\n\nFor a more detailed demonstration of this idea in action, please see the\n[metadata example](./examples/metadata).\n\n## Demos\n\nCode examples are located in [the examples directory](./examples).  Run commands\nare added to the [Makefile](Makefile) for convenience but they should be as\nsimple as `go run ./examples/features/main.go`, etc.  You can also view what\nthey look like by checking the example's directory in each README here on\nGithub.\n\nTo run the examples, clone this repo and run:\n\n```bash\n# Run the pokemon demo for a general feel of common useful features\nmake\n\n# Run dimensions example to see multiple sizes of simple tables in action\nmake example-dimensions\n\n# Or run any of them directly\ngo run ./examples/pagination/main.go\n```\n\n"
  },
  {
    "path": "examples/dimensions/README.md",
    "content": "# Dimensions\n\nShows some simple tables with various dimensions.\n\n<img width=\"534\" alt=\"image\" src=\"https://user-images.githubusercontent.com/5923958/170801679-92f420d7-5cdc-4c66-8d32-e421b1f5dc18.png\">\n"
  },
  {
    "path": "examples/dimensions/main.go",
    "content": "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/lipgloss\"\n\t\"github.com/evertras/bubble-table/table\"\n)\n\ntype Model struct {\n\ttable3x3 table.Model\n\ttable1x3 table.Model\n\ttable3x1 table.Model\n\ttable1x1 table.Model\n\ttable5x5 table.Model\n}\n\nfunc genTable(columnCount int, rowCount int) table.Model {\n\tcolumns := []table.Column{}\n\n\tfor column := 0; column < columnCount; column++ {\n\t\tcolumnStr := fmt.Sprintf(\"%d\", column+1)\n\t\tcolumns = append(columns, table.NewColumn(columnStr, columnStr, 4))\n\t}\n\n\trows := []table.Row{}\n\n\tfor row := 1; row < rowCount; row++ {\n\t\trowData := table.RowData{}\n\n\t\tfor column := 0; column < columnCount; column++ {\n\t\t\tcolumnStr := fmt.Sprintf(\"%d\", column+1)\n\t\t\trowData[columnStr] = fmt.Sprintf(\"%d,%d\", column+1, row+1)\n\t\t}\n\n\t\trows = append(rows, table.NewRow(rowData))\n\t}\n\n\treturn table.New(columns).WithRows(rows).HeaderStyle(lipgloss.NewStyle().Bold(true))\n}\n\nfunc NewModel() Model {\n\treturn Model{\n\t\ttable1x1: genTable(1, 1),\n\t\ttable3x1: genTable(3, 1),\n\t\ttable1x3: genTable(1, 3),\n\t\ttable3x3: genTable(3, 3),\n\t\ttable5x5: genTable(5, 5),\n\t}\n}\n\nfunc (m Model) Init() tea.Cmd {\n\treturn nil\n}\n\nfunc (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {\n\tvar (\n\t\tcmd  tea.Cmd\n\t\tcmds []tea.Cmd\n\t)\n\n\tm.table1x1, cmd = m.table1x1.Update(msg)\n\tcmds = append(cmds, cmd)\n\n\tm.table3x1, cmd = m.table3x1.Update(msg)\n\tcmds = append(cmds, cmd)\n\n\tm.table1x3, cmd = m.table1x3.Update(msg)\n\tcmds = append(cmds, cmd)\n\n\tm.table3x3, cmd = m.table3x3.Update(msg)\n\tcmds = append(cmds, cmd)\n\n\tm.table5x5, cmd = m.table5x5.Update(msg)\n\tcmds = append(cmds, cmd)\n\n\tswitch msg := msg.(type) {\n\tcase tea.KeyMsg:\n\t\tswitch msg.String() {\n\t\tcase \"ctrl+c\", \"esc\", \"q\":\n\t\t\tcmds = append(cmds, tea.Quit)\n\t\t}\n\t}\n\n\treturn m, tea.Batch(cmds...)\n}\n\nfunc (m Model) View() string {\n\tbody := strings.Builder{}\n\n\tbody.WriteString(\"Table demo with various sized tables!\\nPress q or ctrl+c to quit\\n\")\n\n\tpad := lipgloss.NewStyle().Padding(1)\n\n\ttablesSmall := lipgloss.JoinHorizontal(\n\t\tlipgloss.Top,\n\t\tpad.Render(m.table1x1.View()),\n\t\tpad.Render(m.table1x3.View()),\n\t\tpad.Render(m.table3x1.View()),\n\t\tpad.Render(m.table3x3.View()),\n\t)\n\n\ttableBig := pad.Render(m.table5x5.View())\n\n\tbody.WriteString(lipgloss.JoinVertical(lipgloss.Center, tablesSmall, tableBig))\n\n\treturn body.String()\n}\n\nfunc main() {\n\tp := tea.NewProgram(NewModel())\n\n\tif err := p.Start(); err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n"
  },
  {
    "path": "examples/events/README.md",
    "content": "# Events example\n\nThis example shows how to use events to handle triggers for data retrieval or\nother desired behavior.  This example in particular shows how to use events to\ntrigger data retrieval.\n\n<img width=\"290\" alt=\"image\" src=\"https://user-images.githubusercontent.com/5923958/173168499-836bd03b-debb-455e-9f73-f2bfd028f2b2.png\">\n"
  },
  {
    "path": "examples/events/main.go",
    "content": "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/lipgloss\"\n\t\"github.com/evertras/bubble-table/table\"\n)\n\ntype Element string\n\nconst (\n\tcolumnKeyName    = \"name\"\n\tcolumnKeyElement = \"element\"\n\n\t// This is not a visible column, but is used to attach useful reference data\n\t// to the row itself for easier retrieval\n\tcolumnKeyPokemonData = \"pokedata\"\n\n\telementNormal   Element = \"Normal\"\n\telementFire     Element = \"Fire\"\n\telementElectric Element = \"Electric\"\n\telementWater    Element = \"Water\"\n\telementPlant    Element = \"Plant\"\n)\n\nvar (\n\tstyleSubtle = lipgloss.NewStyle().Foreground(lipgloss.Color(\"#888\"))\n\n\tstyleBase = lipgloss.NewStyle().\n\t\t\tForeground(lipgloss.Color(\"#a7a\")).\n\t\t\tBorderForeground(lipgloss.Color(\"#a38\")).\n\t\t\tAlign(lipgloss.Right)\n\n\telementColors = map[Element]string{\n\t\telementNormal:   \"#fa0\",\n\t\telementFire:     \"#f64\",\n\t\telementElectric: \"#ff0\",\n\t\telementWater:    \"#44f\",\n\t\telementPlant:    \"#8b8\",\n\t}\n)\n\ntype Pokemon struct {\n\tName                     string\n\tElement                  Element\n\tConversationCount        int\n\tPositiveSentimentPercent float32\n\tNegativeSentimentPercent float32\n}\n\nfunc NewPokemon(name string, element Element, conversationCount int, positiveSentimentPercent float32, negativeSentimentPercent float32) Pokemon {\n\treturn Pokemon{\n\t\tName:                     name,\n\t\tElement:                  element,\n\t\tConversationCount:        conversationCount,\n\t\tPositiveSentimentPercent: positiveSentimentPercent,\n\t\tNegativeSentimentPercent: negativeSentimentPercent,\n\t}\n}\n\nfunc (p Pokemon) ToRow() table.Row {\n\tcolor, exists := elementColors[p.Element]\n\n\tif !exists {\n\t\tcolor = elementColors[elementNormal]\n\t}\n\n\treturn table.NewRow(table.RowData{\n\t\tcolumnKeyName:    p.Name,\n\t\tcolumnKeyElement: table.NewStyledCell(p.Element, lipgloss.NewStyle().Foreground(lipgloss.Color(color))),\n\n\t\t// This isn't a visible column, but we can add the data here anyway for later retrieval\n\t\tcolumnKeyPokemonData: p,\n\t})\n}\n\ntype Model struct {\n\tpokeTable table.Model\n\n\tcurrentPokemonData Pokemon\n\n\tlastSelectedEvent table.UserEventRowSelectToggled\n}\n\nfunc NewModel() Model {\n\tpokemon := []Pokemon{\n\t\tNewPokemon(\"Pikachu\", elementElectric, 2300648, 21.9, 8.54),\n\t\tNewPokemon(\"Eevee\", elementNormal, 636373, 26.4, 7.37),\n\t\tNewPokemon(\"Bulbasaur\", elementPlant, 352190, 25.7, 9.02),\n\t\tNewPokemon(\"Squirtle\", elementWater, 241259, 25.6, 5.96),\n\t\tNewPokemon(\"Blastoise\", elementWater, 162794, 19.5, 6.04),\n\t\tNewPokemon(\"Charmander\", elementFire, 265760, 31.2, 5.25),\n\t\tNewPokemon(\"Charizard\", elementFire, 567763, 25.6, 7.56),\n\t}\n\n\trows := []table.Row{}\n\n\tfor _, p := range pokemon {\n\t\trows = append(rows, p.ToRow())\n\t}\n\n\treturn Model{\n\t\tpokeTable: table.New([]table.Column{\n\t\t\ttable.NewColumn(columnKeyName, \"Name\", 13),\n\t\t\ttable.NewColumn(columnKeyElement, \"Element\", 10),\n\t\t}).WithRows(rows).\n\t\t\tBorderRounded().\n\t\t\tWithBaseStyle(styleBase).\n\t\t\tWithPageSize(4).\n\t\t\tFocused(true).\n\t\t\tSelectableRows(true),\n\t\tcurrentPokemonData: pokemon[0],\n\t}\n}\n\nfunc (m Model) Init() tea.Cmd {\n\treturn nil\n}\n\nfunc (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {\n\tvar (\n\t\tcmd  tea.Cmd\n\t\tcmds []tea.Cmd\n\t)\n\n\tm.pokeTable, cmd = m.pokeTable.Update(msg)\n\tcmds = append(cmds, cmd)\n\n\tfor _, e := range m.pokeTable.GetLastUpdateUserEvents() {\n\t\tswitch e := e.(type) {\n\t\tcase table.UserEventHighlightedIndexChanged:\n\t\t\t// We can pretend this is an async data retrieval, but really we already\n\t\t\t// have the data, so just return it after some fake delay.  Also note\n\t\t\t// that the event has some data attached to it, but we're ignoring\n\t\t\t// that for this example as we just want the current highlighted row.\n\t\t\tselectedPokemon := m.pokeTable.HighlightedRow().Data[columnKeyPokemonData].(Pokemon)\n\n\t\t\tcmds = append(cmds, func() tea.Msg {\n\t\t\t\ttime.Sleep(time.Millisecond * 200)\n\n\t\t\t\treturn selectedPokemon\n\t\t\t})\n\n\t\tcase table.UserEventRowSelectToggled:\n\t\t\tm.lastSelectedEvent = e\n\t\t}\n\t}\n\n\tswitch msg := msg.(type) {\n\tcase tea.KeyMsg:\n\t\tswitch msg.String() {\n\t\tcase \"ctrl+c\", \"esc\", \"q\":\n\t\t\tcmds = append(cmds, tea.Quit)\n\t\t}\n\n\tcase Pokemon:\n\t\tm.currentPokemonData = msg\n\t}\n\n\treturn m, tea.Batch(cmds...)\n}\n\nfunc (m Model) View() string {\n\tview := lipgloss.JoinVertical(\n\t\tlipgloss.Left,\n\t\tstyleSubtle.Render(\"Press q or ctrl+c to quit\"),\n\t\tfmt.Sprintf(\"Highlighted (200 ms delay): %s (%s)\", m.currentPokemonData.Name, m.currentPokemonData.Element),\n\t\tfmt.Sprintf(\"Last selected event: %d (%v)\", m.lastSelectedEvent.RowIndex, m.lastSelectedEvent.IsSelected),\n\t\tlipgloss.NewStyle().Foreground(lipgloss.Color(\"#8c8\")).\n\t\t\tRender(\":D %\"+fmt.Sprintf(\"%.1f\", m.currentPokemonData.PositiveSentimentPercent)),\n\t\tlipgloss.NewStyle().Foreground(lipgloss.Color(\"#c88\")).\n\t\t\tRender(\":( %\"+fmt.Sprintf(\"%.1f\", m.currentPokemonData.NegativeSentimentPercent)),\n\t\tm.pokeTable.View(),\n\t) + \"\\n\"\n\n\treturn lipgloss.NewStyle().MarginLeft(1).Render(view)\n}\n\nfunc main() {\n\tp := tea.NewProgram(NewModel())\n\n\tif err := p.Start(); err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n"
  },
  {
    "path": "examples/features/README.md",
    "content": "# Full feature example\n\nThis table contains most of the implemented features, but as more features have\nbeen added some have not made it into this demo for practical purposes.  This\ncan be a useful reference for various features even if it's not a very\nappealing final product.\n\n<img width=\"593\" alt=\"image\" src=\"https://user-images.githubusercontent.com/5923958/170802611-84d15b28-0c57-4095-a321-d6ba05af58f8.png\">\n"
  },
  {
    "path": "examples/features/main.go",
    "content": "// This file contains a full demo of most available features, for both testing\n// and for reference\npackage main\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"strings\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/charmbracelet/lipgloss\"\n\t\"github.com/evertras/bubble-table/table\"\n)\n\nconst (\n\tcolumnKeyID          = \"id\"\n\tcolumnKeyName        = \"name\"\n\tcolumnKeyDescription = \"description\"\n\tcolumnKeyCount       = \"count\"\n)\n\nvar (\n\tcustomBorder = table.Border{\n\t\tTop:    \"─\",\n\t\tLeft:   \"│\",\n\t\tRight:  \"│\",\n\t\tBottom: \"─\",\n\n\t\tTopRight:    \"╮\",\n\t\tTopLeft:     \"╭\",\n\t\tBottomRight: \"╯\",\n\t\tBottomLeft:  \"╰\",\n\n\t\tTopJunction:    \"╥\",\n\t\tLeftJunction:   \"├\",\n\t\tRightJunction:  \"┤\",\n\t\tBottomJunction: \"╨\",\n\t\tInnerJunction:  \"╫\",\n\n\t\tInnerDivider: \"║\",\n\t}\n)\n\ntype Model struct {\n\ttableModel table.Model\n}\n\nfunc NewModel() Model {\n\tcolumns := []table.Column{\n\t\ttable.NewColumn(columnKeyID, \"ID\", 5).WithStyle(\n\t\t\tlipgloss.NewStyle().\n\t\t\t\tFaint(true).\n\t\t\t\tForeground(lipgloss.Color(\"#88f\")).\n\t\t\t\tAlign(lipgloss.Center)),\n\t\ttable.NewColumn(columnKeyName, \"Name\", 10),\n\t\ttable.NewColumn(columnKeyDescription, \"Description\", 30),\n\t\ttable.NewColumn(columnKeyCount, \"#\", 5),\n\t}\n\n\trows := []table.Row{\n\t\ttable.NewRow(table.RowData{\n\t\t\tcolumnKeyID: \"abc\",\n\t\t\t// Missing name\n\t\t\tcolumnKeyDescription: \"The first table entry, ever\",\n\t\t\tcolumnKeyCount:       4,\n\t\t}),\n\t\ttable.NewRow(table.RowData{\n\t\t\tcolumnKeyID:          \"123\",\n\t\t\tcolumnKeyName:        \"Oh no\",\n\t\t\tcolumnKeyDescription: \"Super bold!\",\n\t\t\tcolumnKeyCount:       17,\n\t\t}).WithStyle(lipgloss.NewStyle().Foreground(lipgloss.Color(\"9\")).Bold(true)),\n\t\ttable.NewRow(table.RowData{\n\t\t\tcolumnKeyID: \"def\",\n\t\t\t// Apply a style to this cell\n\t\t\tcolumnKeyName:        table.NewStyledCell(\"Styled\", lipgloss.NewStyle().Foreground(lipgloss.Color(\"#8ff\"))),\n\t\t\tcolumnKeyDescription: \"This is a really, really, really long description that will get cut off\",\n\t\t\tcolumnKeyCount:       table.NewStyledCell(0, lipgloss.NewStyle().Faint(true)),\n\t\t}),\n\t\ttable.NewRow(table.RowData{\n\t\t\tcolumnKeyID:          \"spg\",\n\t\t\tcolumnKeyName:        \"Page 2\",\n\t\t\tcolumnKeyDescription: \"Second page\",\n\t\t\tcolumnKeyCount:       2,\n\t\t}),\n\t\ttable.NewRow(table.RowData{\n\t\t\tcolumnKeyID:          \"spg2\",\n\t\t\tcolumnKeyName:        \"Page 2.1\",\n\t\t\tcolumnKeyDescription: \"Second page again\",\n\t\t\tcolumnKeyCount:       4,\n\t\t}),\n\t}\n\n\t// Start with the default key map and change it slightly, just for demoing\n\tkeys := table.DefaultKeyMap()\n\tkeys.RowDown.SetKeys(\"j\", \"down\", \"s\")\n\tkeys.RowUp.SetKeys(\"k\", \"up\", \"w\")\n\n\tmodel := Model{\n\t\t// Throw features in... the point is not to look good, it's just reference!\n\t\ttableModel: table.New(columns).\n\t\t\tWithRows(rows).\n\t\t\tHeaderStyle(lipgloss.NewStyle().Foreground(lipgloss.Color(\"10\")).Bold(true)).\n\t\t\tSelectableRows(true).\n\t\t\tFocused(true).\n\t\t\tBorder(customBorder).\n\t\t\tWithKeyMap(keys).\n\t\t\tWithStaticFooter(\"Footer!\").\n\t\t\tWithPageSize(3).\n\t\t\tWithSelectedText(\" \", \"✓\").\n\t\t\tWithBaseStyle(\n\t\t\t\tlipgloss.NewStyle().\n\t\t\t\t\tBorderForeground(lipgloss.Color(\"#a38\")).\n\t\t\t\t\tForeground(lipgloss.Color(\"#a7a\")).\n\t\t\t\t\tAlign(lipgloss.Left),\n\t\t\t).\n\t\t\tSortByAsc(columnKeyID).\n\t\t\tWithMissingDataIndicatorStyled(table.StyledCell{\n\t\t\t\tStyle: lipgloss.NewStyle().Foreground(lipgloss.Color(\"#faa\")),\n\t\t\t\tData:  \"<ない>\",\n\t\t\t}),\n\t}\n\n\tmodel.updateFooter()\n\n\treturn model\n}\n\nfunc (m Model) Init() tea.Cmd {\n\treturn nil\n}\n\nfunc (m *Model) updateFooter() {\n\thighlightedRow := m.tableModel.HighlightedRow()\n\n\tfooterText := fmt.Sprintf(\n\t\t\"Pg. %d/%d - Currently looking at ID: %s\",\n\t\tm.tableModel.CurrentPage(),\n\t\tm.tableModel.MaxPages(),\n\t\thighlightedRow.Data[columnKeyID],\n\t)\n\n\tm.tableModel = m.tableModel.WithStaticFooter(footerText)\n}\n\nfunc (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {\n\tvar (\n\t\tcmd  tea.Cmd\n\t\tcmds []tea.Cmd\n\t)\n\n\tm.tableModel, cmd = m.tableModel.Update(msg)\n\tcmds = append(cmds, cmd)\n\n\t// We control the footer text, so make sure to update it\n\tm.updateFooter()\n\n\tswitch msg := msg.(type) {\n\tcase tea.KeyMsg:\n\t\tswitch msg.String() {\n\t\tcase \"ctrl+c\", \"esc\", \"q\":\n\t\t\tcmds = append(cmds, tea.Quit)\n\n\t\tcase \"i\":\n\t\t\tm.tableModel = m.tableModel.WithHeaderVisibility(!m.tableModel.GetHeaderVisibility())\n\t\t}\n\t}\n\n\treturn m, tea.Batch(cmds...)\n}\n\nfunc (m Model) View() string {\n\tbody := strings.Builder{}\n\n\tbody.WriteString(\"A (chaotic) table demo with all features enabled!\\n\")\n\tbody.WriteString(\"Press left/right or page up/down to move pages\\n\")\n\tbody.WriteString(\"Press 'i' to toggle the header visibility\\n\")\n\tbody.WriteString(\"Press space/enter to select a row, q or ctrl+c to quit\\n\")\n\n\tselectedIDs := []string{}\n\n\tfor _, row := range m.tableModel.SelectedRows() {\n\t\t// Slightly dangerous type assumption but fine for demo\n\t\tselectedIDs = append(selectedIDs, row.Data[columnKeyID].(string))\n\t}\n\n\tbody.WriteString(fmt.Sprintf(\"SelectedIDs: %s\\n\", strings.Join(selectedIDs, \", \")))\n\n\tbody.WriteString(m.tableModel.View())\n\n\tbody.WriteString(\"\\n\")\n\n\treturn body.String()\n}\n\nfunc main() {\n\tp := tea.NewProgram(NewModel())\n\n\tif err := p.Start(); err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n"
  },
  {
    "path": "examples/filter/README.md",
    "content": "# Filter example\n\nShows how the table can use a built-in filter to filter results.\n\n<img width=\"1052\" alt=\"image\" src=\"https://user-images.githubusercontent.com/5923958/170801744-a6488a8b-54f7-4132-b2c3-0e0d7166d902.png\">\n"
  },
  {
    "path": "examples/filter/main.go",
    "content": "package main\n\nimport (\n\t\"log\"\n\t\"strings\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/evertras/bubble-table/table\"\n)\n\nconst (\n\tcolumnKeyTitle       = \"title\"\n\tcolumnKeyAuthor      = \"author\"\n\tcolumnKeyDescription = \"description\"\n)\n\ntype Model struct {\n\ttable table.Model\n}\n\nfunc NewModel() Model {\n\tcolumns := []table.Column{\n\t\ttable.NewColumn(columnKeyTitle, \"Title\", 13).WithFiltered(true),\n\t\ttable.NewColumn(columnKeyAuthor, \"Author\", 13).WithFiltered(true),\n\t\ttable.NewColumn(columnKeyDescription, \"Description\", 50),\n\t}\n\treturn Model{\n\t\ttable: table.\n\t\t\tNew(columns).\n\t\t\tFiltered(true).\n\t\t\tFocused(true).\n\t\t\tWithPageSize(10).\n\t\t\tSelectableRows(true).\n\t\t\tWithRows([]table.Row{\n\t\t\t\ttable.NewRow(table.RowData{\n\t\t\t\t\tcolumnKeyTitle:       \"Computer Systems : A Programmer's Perspective\",\n\t\t\t\t\tcolumnKeyAuthor:      \"Randal E. Bryant、David R. O'Hallaron / Prentice Hall \",\n\t\t\t\t\tcolumnKeyDescription: \"This book explains the important and enduring concepts underlying all computer...\",\n\t\t\t\t}),\n\t\t\t\ttable.NewRow(table.RowData{\n\t\t\t\t\tcolumnKeyTitle:       \"Effective Java : 3rd Edition\",\n\t\t\t\t\tcolumnKeyAuthor:      \"Joshua Bloch\",\n\t\t\t\t\tcolumnKeyDescription: \"The Definitive Guide to Java Platform Best Practices—Updated for Java 9 Java ...\",\n\t\t\t\t}),\n\t\t\t\ttable.NewRow(table.RowData{\n\t\t\t\t\tcolumnKeyTitle:       \"Structure and Interpretation of Computer Programs - 2nd Edition (MIT)\",\n\t\t\t\t\tcolumnKeyAuthor:      \"Harold Abelson、Gerald Jay Sussman\",\n\t\t\t\t\tcolumnKeyDescription: \"Structure and Interpretation of Computer Programs has had a dramatic impact on...\",\n\t\t\t\t}),\n\t\t\t\ttable.NewRow(table.RowData{\n\t\t\t\t\tcolumnKeyTitle:       \"Game Programming Patterns\",\n\t\t\t\t\tcolumnKeyAuthor:      \"Robert Nystrom / Genever Benning\",\n\t\t\t\t\tcolumnKeyDescription: \"The biggest challenge facing many game programmers is completing their game. M...\",\n\t\t\t\t}),\n\t\t\t}),\n\t}\n}\n\nfunc (m Model) Init() tea.Cmd {\n\treturn nil\n}\n\nfunc (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {\n\tvar (\n\t\tcmd  tea.Cmd\n\t\tcmds []tea.Cmd\n\t)\n\n\tm.table, cmd = m.table.Update(msg)\n\tcmds = append(cmds, cmd)\n\n\tswitch msg := msg.(type) {\n\tcase tea.KeyMsg:\n\t\tswitch msg.String() {\n\t\tcase \"ctrl+c\", \"q\":\n\t\t\tif !m.table.GetIsFilterInputFocused() {\n\t\t\t\tcmds = append(cmds, tea.Quit)\n\t\t\t}\n\t\t}\n\n\t}\n\n\treturn m, tea.Batch(cmds...)\n}\n\nfunc (m Model) View() string {\n\tbody := strings.Builder{}\n\n\tbody.WriteString(\"A filtered simple default table\\n\" +\n\t\t\"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\")\n\n\tbody.WriteString(m.table.View())\n\n\treturn body.String()\n}\n\nfunc main() {\n\tp := tea.NewProgram(NewModel())\n\n\tif err := p.Start(); err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n"
  },
  {
    "path": "examples/filterapi/README.md",
    "content": "# Filter API example\n\nSimilar to the [regular filter example](../filter/), but uses an external text\nbox control for filtering.  This demonstrates how you can use your own text\ncontrols to perform filtering for more flexible UIs.\n\n<img width=\"1052\" alt=\"image\" src=\"https://user-images.githubusercontent.com/5923958/170801860-9bfac90e-00aa-4591-8799-00f67836e6b8.png\">\n"
  },
  {
    "path": "examples/filterapi/main.go",
    "content": "package main\n\nimport (\n\t\"github.com/charmbracelet/bubbles/textinput\"\n\t\"log\"\n\t\"strings\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/evertras/bubble-table/table\"\n)\n\nconst (\n\tcolumnKeyTitle       = \"title\"\n\tcolumnKeyAuthor      = \"author\"\n\tcolumnKeyDescription = \"description\"\n)\n\ntype Model struct {\n\ttable           table.Model\n\tfilterTextInput textinput.Model\n}\n\nfunc NewModel() Model {\n\tcolumns := []table.Column{\n\t\ttable.NewColumn(columnKeyTitle, \"Title\", 13).WithFiltered(true),\n\t\ttable.NewColumn(columnKeyAuthor, \"Author\", 13).WithFiltered(true),\n\t\ttable.NewColumn(columnKeyDescription, \"Description\", 50),\n\t}\n\n\treturn Model{\n\t\ttable: table.\n\t\t\tNew(columns).\n\t\t\tFiltered(true).\n\t\t\tFocused(true).\n\t\t\tWithFooterVisibility(false).\n\t\t\tWithPageSize(10).\n\t\t\tWithRows([]table.Row{\n\t\t\t\ttable.NewRow(table.RowData{\n\t\t\t\t\tcolumnKeyTitle:       \"Computer Systems : A Programmer's Perspective\",\n\t\t\t\t\tcolumnKeyAuthor:      \"Randal E. Bryant、David R. O'Hallaron / Prentice Hall \",\n\t\t\t\t\tcolumnKeyDescription: \"This book explains the important and enduring concepts underlying all computer...\",\n\t\t\t\t}),\n\t\t\t\ttable.NewRow(table.RowData{\n\t\t\t\t\tcolumnKeyTitle:       \"Effective Java : 3rd Edition\",\n\t\t\t\t\tcolumnKeyAuthor:      \"Joshua Bloch\",\n\t\t\t\t\tcolumnKeyDescription: \"The Definitive Guide to Java Platform Best Practices—Updated for Java 9 Java ...\",\n\t\t\t\t}),\n\t\t\t\ttable.NewRow(table.RowData{\n\t\t\t\t\tcolumnKeyTitle:       \"Structure and Interpretation of Computer Programs - 2nd Edition (MIT)\",\n\t\t\t\t\tcolumnKeyAuthor:      \"Harold Abelson、Gerald Jay Sussman\",\n\t\t\t\t\tcolumnKeyDescription: \"Structure and Interpretation of Computer Programs has had a dramatic impact on...\",\n\t\t\t\t}),\n\t\t\t\ttable.NewRow(table.RowData{\n\t\t\t\t\tcolumnKeyTitle:       \"Game Programming Patterns\",\n\t\t\t\t\tcolumnKeyAuthor:      \"Robert Nystrom / Genever Benning\",\n\t\t\t\t\tcolumnKeyDescription: \"The biggest challenge facing many game programmers is completing their game. M...\",\n\t\t\t\t}),\n\t\t\t}),\n\t\tfilterTextInput: textinput.New(),\n\t}\n}\n\nfunc (m Model) Init() tea.Cmd {\n\treturn nil\n}\n\nfunc (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {\n\tvar (\n\t\tcmd  tea.Cmd\n\t\tcmds []tea.Cmd\n\t)\n\n\tswitch msg := msg.(type) {\n\tcase tea.KeyMsg:\n\t\t// global\n\t\tif msg.String() == \"ctrl+c\" {\n\t\t\tcmds = append(cmds, tea.Quit)\n\n\t\t\treturn m, tea.Batch(cmds...)\n\t\t}\n\t\t// event to filter\n\t\tif m.filterTextInput.Focused() {\n\t\t\tif msg.String() == \"enter\" {\n\t\t\t\tm.filterTextInput.Blur()\n\t\t\t} else {\n\t\t\t\tm.filterTextInput, _ = m.filterTextInput.Update(msg)\n\t\t\t}\n\t\t\tm.table = m.table.WithFilterInput(m.filterTextInput)\n\n\t\t\treturn m, tea.Batch(cmds...)\n\t\t}\n\n\t\t// others component\n\t\tswitch msg.String() {\n\t\tcase \"/\":\n\t\t\tm.filterTextInput.Focus()\n\t\tcase \"q\":\n\t\t\tcmds = append(cmds, tea.Quit)\n\t\t\treturn m, tea.Batch(cmds...)\n\t\tdefault:\n\t\t\tm.table, cmd = m.table.Update(msg)\n\t\t\tcmds = append(cmds, cmd)\n\t\t}\n\t}\n\n\treturn m, tea.Batch(cmds...)\n}\n\nfunc (m Model) View() string {\n\tbody := strings.Builder{}\n\n\tbody.WriteString(\"A filtered simple default table\\n\" +\n\t\t\"Currently filter by Title and Author, press / + letters to start filtering, and escape to clear filter.\\n\" +\n\t\t\"Press q or ctrl+c to quit\\n\\n\")\n\n\tbody.WriteString(m.filterTextInput.View() + \"\\n\")\n\tbody.WriteString(m.table.View())\n\n\treturn body.String()\n}\n\nfunc main() {\n\tp := tea.NewProgram(NewModel())\n\n\tif err := p.Start(); err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n"
  },
  {
    "path": "examples/flex/README.md",
    "content": "# Flex example\n\nThis example shows how to use flexible-width columns.  The example stretches to\nfill the full width of the terminal whenever it's resized.\n\n<img width=\"904\" alt=\"image\" src=\"https://user-images.githubusercontent.com/5923958/170801927-36599e10-7d25-4b8f-94d1-7052c25f572d.png\">\n"
  },
  {
    "path": "examples/flex/main.go",
    "content": "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\"github.com/evertras/bubble-table/table\"\n)\n\nconst (\n\tcolumnKeyName        = \"name\"\n\tcolumnKeyElement     = \"element\"\n\tcolumnKeyDescription = \"description\"\n\n\tminWidth  = 30\n\tminHeight = 8\n\n\t// Add a fixed margin to account for description & instructions\n\tfixedVerticalMargin = 4\n)\n\ntype Model struct {\n\tflexTable table.Model\n\n\t// Window dimensions\n\ttotalWidth  int\n\ttotalHeight int\n\n\t// Table dimensions\n\thorizontalMargin int\n\tverticalMargin   int\n}\n\nfunc NewModel() Model {\n\treturn Model{\n\t\tflexTable: table.New([]table.Column{\n\t\t\ttable.NewColumn(columnKeyName, \"Name\", 10),\n\t\t\t// This table uses flex columns, but it will still need a target\n\t\t\t// width in order to know what width it should fill.  In this example\n\t\t\t// the target width is set below in `recalculateTable`, which sets\n\t\t\t// the table to the width of the screen to demonstrate resizing\n\t\t\t// with flex columns.\n\t\t\ttable.NewFlexColumn(columnKeyElement, \"Element\", 1),\n\t\t\ttable.NewFlexColumn(columnKeyDescription, \"Description\", 3),\n\t\t}).WithRows([]table.Row{\n\t\t\ttable.NewRow(table.RowData{\n\t\t\t\tcolumnKeyName:        \"Pikachu\",\n\t\t\t\tcolumnKeyElement:     \"Electric\",\n\t\t\t\tcolumnKeyDescription: \"Super zappy mouse, handle with care\",\n\t\t\t}),\n\t\t\ttable.NewRow(table.RowData{\n\t\t\t\tcolumnKeyName:        \"Charmander\",\n\t\t\t\tcolumnKeyElement:     \"Fire\",\n\t\t\t\tcolumnKeyDescription: \"直立した恐竜のような身体と、尻尾の先端に常に燃えている炎が特徴。\",\n\t\t\t}),\n\t\t}).WithStaticFooter(\"A footer!\"),\n\t}\n}\n\nfunc (m Model) Init() tea.Cmd {\n\treturn nil\n}\n\nfunc (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {\n\tvar (\n\t\tcmd  tea.Cmd\n\t\tcmds []tea.Cmd\n\t)\n\n\tm.flexTable, cmd = m.flexTable.Update(msg)\n\tcmds = append(cmds, cmd)\n\n\tswitch msg := msg.(type) {\n\tcase tea.KeyMsg:\n\t\tswitch msg.String() {\n\t\tcase \"ctrl+c\", \"esc\", \"q\":\n\t\t\tcmds = append(cmds, tea.Quit)\n\n\t\tcase \"left\":\n\t\t\tif m.calculateWidth() > minWidth {\n\t\t\t\tm.horizontalMargin++\n\t\t\t\tm.recalculateTable()\n\t\t\t}\n\n\t\tcase \"right\":\n\t\t\tif m.horizontalMargin > 0 {\n\t\t\t\tm.horizontalMargin--\n\t\t\t\tm.recalculateTable()\n\t\t\t}\n\n\t\tcase \"up\":\n\t\t\tif m.calculateHeight() > minHeight {\n\t\t\t\tm.verticalMargin++\n\t\t\t\tm.recalculateTable()\n\t\t\t}\n\n\t\tcase \"down\":\n\t\t\tif m.verticalMargin > 0 {\n\t\t\t\tm.verticalMargin--\n\t\t\t\tm.recalculateTable()\n\t\t\t}\n\t\t}\n\n\tcase tea.WindowSizeMsg:\n\t\tm.totalWidth = msg.Width\n\t\tm.totalHeight = msg.Height\n\n\t\tm.recalculateTable()\n\t}\n\n\treturn m, tea.Batch(cmds...)\n}\n\nfunc (m *Model) recalculateTable() {\n\tm.flexTable = m.flexTable.\n\t\tWithTargetWidth(m.calculateWidth()).\n\t\tWithMinimumHeight(m.calculateHeight())\n}\n\nfunc (m Model) calculateWidth() int {\n\treturn m.totalWidth - m.horizontalMargin\n}\n\nfunc (m Model) calculateHeight() int {\n\treturn m.totalHeight - m.verticalMargin - fixedVerticalMargin\n}\n\nfunc (m Model) View() string {\n\tstrs := []string{\n\t\t\"A flexible table that fills available space (Name column is fixed-width)\",\n\t\tfmt.Sprintf(\"Target size: %d W ⨉ %d H (arrow keys to adjust)\",\n\t\t\tm.calculateWidth(), m.calculateHeight()),\n\t\t\"Press q or ctrl+c to quit\",\n\t\tm.flexTable.View(),\n\t}\n\n\treturn lipgloss.JoinVertical(lipgloss.Left, strs...) + \"\\n\"\n}\n\nfunc main() {\n\tp := tea.NewProgram(NewModel())\n\n\tif err := p.Start(); err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n"
  },
  {
    "path": "examples/metadata/README.md",
    "content": "# Metadata example\n\nThis is the [pokemon example](../pokemon) with metadata attached to the rows in\norder to retrieve data, instead of retrieving the data directly from the row.\nThis can be a useful technique to make more natural data transformations.\n\n![Styled table](https://user-images.githubusercontent.com/5923958/156778142-cc1a32e1-1b1e-4a65-b699-187f39f0f946.png)\n"
  },
  {
    "path": "examples/metadata/main.go",
    "content": "// 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/charmbracelet/bubbletea\"\n\t\"github.com/charmbracelet/lipgloss\"\n\t\"github.com/evertras/bubble-table/table\"\n)\n\ntype Element string\n\nconst (\n\tcolumnKeyName              = \"name\"\n\tcolumnKeyElement           = \"element\"\n\tcolumnKeyConversations     = \"convos\"\n\tcolumnKeyPositiveSentiment = \"positive\"\n\tcolumnKeyNegativeSentiment = \"negative\"\n\n\t// This is not a visible column, but is used to attach useful reference data\n\t// to the row itself for easier retrieval\n\tcolumnKeyPokemonData = \"pokedata\"\n\n\telementNormal   Element = \"Normal\"\n\telementFire     Element = \"Fire\"\n\telementElectric Element = \"Electric\"\n\telementWater    Element = \"Water\"\n\telementPlant    Element = \"Plant\"\n)\n\nvar (\n\tstyleSubtle = lipgloss.NewStyle().Foreground(lipgloss.Color(\"#888\"))\n\n\tstyleBase = lipgloss.NewStyle().\n\t\t\tForeground(lipgloss.Color(\"#a7a\")).\n\t\t\tBorderForeground(lipgloss.Color(\"#a38\")).\n\t\t\tAlign(lipgloss.Right)\n\n\telementColors = map[Element]string{\n\t\telementNormal:   \"#fa0\",\n\t\telementFire:     \"#f64\",\n\t\telementElectric: \"#ff0\",\n\t\telementWater:    \"#44f\",\n\t\telementPlant:    \"#8b8\",\n\t}\n)\n\ntype Pokemon struct {\n\tName                     string\n\tElement                  Element\n\tConversationCount        int\n\tPositiveSentimentPercent float32\n\tNegativeSentimentPercent float32\n}\n\nfunc NewPokemon(name string, element Element, conversationCount int, positiveSentimentPercent float32, negativeSentimentPercent float32) Pokemon {\n\treturn Pokemon{\n\t\tName:                     name,\n\t\tElement:                  element,\n\t\tConversationCount:        conversationCount,\n\t\tPositiveSentimentPercent: positiveSentimentPercent,\n\t\tNegativeSentimentPercent: negativeSentimentPercent,\n\t}\n}\n\nfunc (p Pokemon) ToRow() table.Row {\n\tcolor, exists := elementColors[p.Element]\n\n\tif !exists {\n\t\tcolor = elementColors[elementNormal]\n\t}\n\n\treturn table.NewRow(table.RowData{\n\t\tcolumnKeyName:              p.Name,\n\t\tcolumnKeyElement:           table.NewStyledCell(p.Element, lipgloss.NewStyle().Foreground(lipgloss.Color(color))),\n\t\tcolumnKeyConversations:     p.ConversationCount,\n\t\tcolumnKeyPositiveSentiment: p.PositiveSentimentPercent,\n\t\tcolumnKeyNegativeSentiment: p.NegativeSentimentPercent,\n\n\t\t// This isn't a visible column, but we can add the data here anyway for later retrieval\n\t\tcolumnKeyPokemonData: p,\n\t})\n}\n\ntype Model struct {\n\tpokeTable table.Model\n}\n\nfunc NewModel() Model {\n\tpokemon := []Pokemon{\n\t\tNewPokemon(\"Pikachu\", elementElectric, 2300648, 21.9, 8.54),\n\t\tNewPokemon(\"Eevee\", elementNormal, 636373, 26.4, 7.37),\n\t\tNewPokemon(\"Bulbasaur\", elementPlant, 352190, 25.7, 9.02),\n\t\tNewPokemon(\"Squirtle\", elementWater, 241259, 25.6, 5.96),\n\t\tNewPokemon(\"Blastoise\", elementWater, 162794, 19.5, 6.04),\n\t\tNewPokemon(\"Charmander\", elementFire, 265760, 31.2, 5.25),\n\t\tNewPokemon(\"Charizard\", elementFire, 567763, 25.6, 7.56),\n\t}\n\n\trows := []table.Row{}\n\n\tfor _, p := range pokemon {\n\t\trows = append(rows, p.ToRow())\n\t}\n\n\treturn Model{\n\t\tpokeTable: table.New([]table.Column{\n\t\t\ttable.NewColumn(columnKeyName, \"Name\", 13),\n\t\t\ttable.NewColumn(columnKeyElement, \"Element\", 10),\n\t\t\ttable.NewColumn(columnKeyConversations, \"# Conversations\", 15),\n\t\t\ttable.NewColumn(columnKeyPositiveSentiment, \":D %\", 5).WithStyle(lipgloss.NewStyle().Foreground(lipgloss.Color(\"#8c8\"))),\n\t\t\ttable.NewColumn(columnKeyNegativeSentiment, \":( %\", 5).WithStyle(lipgloss.NewStyle().Foreground(lipgloss.Color(\"#c88\"))),\n\t\t}).WithRows(rows).\n\t\t\tBorderRounded().\n\t\t\tWithBaseStyle(styleBase).\n\t\t\tWithPageSize(6).\n\t\t\tSortByDesc(columnKeyConversations).\n\t\t\tFocused(true),\n\t}\n}\n\nfunc (m Model) Init() tea.Cmd {\n\treturn nil\n}\n\nfunc (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {\n\tvar (\n\t\tcmd  tea.Cmd\n\t\tcmds []tea.Cmd\n\t)\n\n\tm.pokeTable, cmd = m.pokeTable.Update(msg)\n\tcmds = append(cmds, cmd)\n\n\tswitch msg := msg.(type) {\n\tcase tea.KeyMsg:\n\t\tswitch msg.String() {\n\t\tcase \"ctrl+c\", \"esc\", \"q\":\n\t\t\tcmds = append(cmds, tea.Quit)\n\t\t}\n\t}\n\n\treturn m, tea.Batch(cmds...)\n}\n\nfunc (m Model) View() string {\n\t// Get the metadata back out of the row\n\tselected := m.pokeTable.HighlightedRow().Data[columnKeyPokemonData].(Pokemon)\n\n\tview := lipgloss.JoinVertical(\n\t\tlipgloss.Left,\n\t\tstyleSubtle.Render(\"Press q or ctrl+c to quit - Sorted by # Conversations\"),\n\t\tstyleSubtle.Render(\"Highlighted: \"+fmt.Sprintf(\"%s (%s)\", selected.Name, selected.Element)),\n\t\tstyleSubtle.Render(\"https://www.nintendolife.com/news/2021/11/these-are-the-most-loved-and-most-hated-pokemon-according-to-a-new-study\"),\n\t\tm.pokeTable.View(),\n\t) + \"\\n\"\n\n\treturn lipgloss.NewStyle().MarginLeft(1).Render(view)\n}\n\nfunc main() {\n\tp := tea.NewProgram(NewModel())\n\n\tif err := p.Start(); err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n"
  },
  {
    "path": "examples/multiline/README.md",
    "content": "# Multiline  Example\n\nThis 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.\n\n<img width=\"593\" alt=\"image\" src=\"https://github.com/Evertras/bubble-table/assets/23465248/3092b6f2-1e75-4c11-85f6-fcbea249d509\">\n"
  },
  {
    "path": "examples/multiline/main.go",
    "content": "package main\n\nimport (\n\t\"log\"\n\t\"strings\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/charmbracelet/lipgloss\"\n\t\"github.com/evertras/bubble-table/table\"\n)\n\nconst (\n\tcolumnKeyName     = \"name\"\n\tcolumnKeyCountry  = \"country\"\n\tcolumnKeyCurrency = \"crurrency\"\n)\n\ntype Model struct {\n\ttableModel table.Model\n}\n\nfunc NewModel() Model {\n\tcolumns := []table.Column{\n\t\ttable.NewColumn(columnKeyName, \"Name\", 10).WithStyle(\n\t\t\tlipgloss.NewStyle().\n\t\t\t\tForeground(lipgloss.Color(\"#88f\")),\n\t\t),\n\t\ttable.NewColumn(columnKeyCountry, \"Country\", 20),\n\t\ttable.NewColumn(columnKeyCurrency, \"Currency\", 10),\n\t}\n\n\trows := []table.Row{\n\t\ttable.NewRow(\n\t\t\ttable.RowData{\n\t\t\t\tcolumnKeyName:     \"Talon Stokes\",\n\t\t\t\tcolumnKeyCountry:  \"Mexico\",\n\t\t\t\tcolumnKeyCurrency: \"$23.17\",\n\t\t\t}),\n\t\ttable.NewRow(\n\t\t\ttable.RowData{\n\t\t\t\tcolumnKeyName:     \"Sonia Shepard\",\n\t\t\t\tcolumnKeyCountry:  \"United States\",\n\t\t\t\tcolumnKeyCurrency: \"$76.47\",\n\t\t\t}),\n\t\ttable.NewRow(\n\t\t\ttable.RowData{\n\t\t\t\tcolumnKeyName:     \"Shad Reed\",\n\t\t\t\tcolumnKeyCountry:  \"Turkey\",\n\t\t\t\tcolumnKeyCurrency: \"$62.99\",\n\t\t\t}),\n\t\ttable.NewRow(\n\t\t\ttable.RowData{\n\t\t\t\tcolumnKeyName:     \"Kibo Clay\",\n\t\t\t\tcolumnKeyCountry:  \"Philippines\",\n\t\t\t\tcolumnKeyCurrency: \"$29.82\",\n\t\t\t}),\n\t\ttable.NewRow(\n\t\t\ttable.RowData{\n\n\t\t\t\tcolumnKeyName:     \"Leslie Kerr\",\n\t\t\t\tcolumnKeyCountry:  \"Singapore\",\n\t\t\t\tcolumnKeyCurrency: \"$70.54\",\n\t\t\t}),\n\t\ttable.NewRow(\n\t\t\ttable.RowData{\n\t\t\t\tcolumnKeyName:     \"Micah Hurst\",\n\t\t\t\tcolumnKeyCountry:  \"Pakistan\",\n\t\t\t\tcolumnKeyCurrency: \"$80.84\",\n\t\t\t}),\n\t\ttable.NewRow(\n\t\t\ttable.RowData{\n\t\t\t\tcolumnKeyName:     \"Dora Miranda\",\n\t\t\t\tcolumnKeyCountry:  \"Colombia\",\n\t\t\t\tcolumnKeyCurrency: \"$34.75\",\n\t\t\t}),\n\t\ttable.NewRow(\n\t\t\ttable.RowData{\n\t\t\t\tcolumnKeyName:     \"Keefe Walters\",\n\t\t\t\tcolumnKeyCountry:  \"China\",\n\t\t\t\tcolumnKeyCurrency: \"$56.82\",\n\t\t\t}),\n\t\ttable.NewRow(\n\t\t\ttable.RowData{\n\t\t\t\tcolumnKeyName:     \"Fujimoto Tarokizaemon no shoutokinori\",\n\t\t\t\tcolumnKeyCountry:  \"Japan\",\n\t\t\t\tcolumnKeyCurrency: \"$89.31\",\n\t\t\t}),\n\t\ttable.NewRow(\n\t\t\ttable.RowData{\n\t\t\t\tcolumnKeyName:     \"Keefe Walters\",\n\t\t\t\tcolumnKeyCountry:  \"China\",\n\t\t\t\tcolumnKeyCurrency: \"$56.82\",\n\t\t\t}),\n\t\ttable.NewRow(\n\t\t\ttable.RowData{\n\t\t\t\tcolumnKeyName:     \"Vincent Sanchez\",\n\t\t\t\tcolumnKeyCountry:  \"Peru\",\n\t\t\t\tcolumnKeyCurrency: \"$71.60\",\n\t\t\t}),\n\t\ttable.NewRow(\n\t\t\ttable.RowData{\n\t\t\t\tcolumnKeyName:     \"Lani Figueroa\",\n\t\t\t\tcolumnKeyCountry:  \"United Kingdom\",\n\t\t\t\tcolumnKeyCurrency: \"$90.67\",\n\t\t\t}),\n\t}\n\n\tmodel := Model{\n\t\ttableModel: table.New(columns).\n\t\t\tWithRows(rows).\n\t\t\tHeaderStyle(lipgloss.NewStyle().Foreground(lipgloss.Color(\"10\")).Bold(true)).\n\t\t\tFocused(true).\n\t\t\tWithBaseStyle(\n\t\t\t\tlipgloss.NewStyle().\n\t\t\t\t\tBorderForeground(lipgloss.Color(\"#a38\")).\n\t\t\t\t\tForeground(lipgloss.Color(\"#a7a\")).\n\t\t\t\t\tAlign(lipgloss.Left),\n\t\t\t).\n\t\t\tWithMultiline(true),\n\t}\n\n\treturn model\n}\n\nfunc (m Model) Init() tea.Cmd {\n\treturn nil\n}\n\nfunc (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {\n\tvar (\n\t\tcmd  tea.Cmd\n\t\tcmds []tea.Cmd\n\t)\n\n\tm.tableModel, cmd = m.tableModel.Update(msg)\n\tcmds = append(cmds, cmd)\n\n\tswitch msg := msg.(type) {\n\tcase tea.KeyMsg:\n\t\tswitch msg.String() {\n\t\tcase \"ctrl+c\", \"esc\", \"q\":\n\t\t\tcmds = append(cmds, tea.Quit)\n\t\t}\n\t}\n\n\treturn m, tea.Batch(cmds...)\n}\n\nfunc (m Model) View() string {\n\tbody := strings.Builder{}\n\n\tbody.WriteString(\"A table demo with multiline feature enabled!\\n\")\n\tbody.WriteString(\"Press up/down or j/k to move around\\n\")\n\tbody.WriteString(m.tableModel.View())\n\tbody.WriteString(\"\\n\")\n\n\treturn body.String()\n}\n\nfunc main() {\n\tp := tea.NewProgram(NewModel())\n\n\tif err := p.Start(); err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n"
  },
  {
    "path": "examples/pagination/README.md",
    "content": "# Pagination example\n\nThis example shows how to paginate data that would be too long to show in a\nsingle screen.  It also shows how to do this with multiple tables and to\nnavigate between them.\n\n<img width=\"1131\" alt=\"image\" src=\"https://user-images.githubusercontent.com/5923958/170802030-6adcc324-7ee0-42c8-ac80-df1b0eb7d08b.png\">\n"
  },
  {
    "path": "examples/pagination/main.go",
    "content": "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/charmbracelet/lipgloss\"\n\t\"github.com/evertras/bubble-table/table\"\n)\n\ntype Model struct {\n\ttableDefault        table.Model\n\ttableWithRowIndices table.Model\n\n\trowCount int\n}\n\nfunc genRows(columnCount int, rowCount int) []table.Row {\n\trows := []table.Row{}\n\n\tfor row := 1; row <= rowCount; row++ {\n\t\trowData := table.RowData{}\n\n\t\tfor column := 0; column < columnCount; column++ {\n\t\t\tcolumnStr := fmt.Sprintf(\"%d\", column+1)\n\t\t\trowData[columnStr] = fmt.Sprintf(\"%d - %d\", column+1, row)\n\t\t}\n\n\t\trows = append(rows, table.NewRow(rowData))\n\t}\n\n\treturn rows\n}\n\nfunc genTable(columnCount int, rowCount int) table.Model {\n\tcolumns := []table.Column{}\n\n\tfor column := 0; column < columnCount; column++ {\n\t\tcolumnStr := fmt.Sprintf(\"%d\", column+1)\n\t\tcolumns = append(columns, table.NewColumn(columnStr, columnStr, 8))\n\t}\n\n\trows := genRows(columnCount, rowCount)\n\n\treturn table.New(columns).WithRows(rows).HeaderStyle(lipgloss.NewStyle().Bold(true))\n}\n\nfunc NewModel() Model {\n\tconst startingRowCount = 105\n\n\tm := Model{\n\t\trowCount:            startingRowCount,\n\t\ttableDefault:        genTable(3, startingRowCount).WithPageSize(10).Focused(true),\n\t\ttableWithRowIndices: genTable(3, startingRowCount).WithPageSize(10).Focused(false),\n\t}\n\n\tm.regenTableRows()\n\n\treturn m\n}\n\nfunc (m *Model) regenTableRows() {\n\tm.tableDefault = m.tableDefault.WithRows(genRows(3, m.rowCount))\n\tm.tableWithRowIndices = m.tableWithRowIndices.WithRows(genRows(3, m.rowCount))\n}\n\nfunc (m Model) Init() tea.Cmd {\n\treturn nil\n}\n\nfunc (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {\n\tvar (\n\t\tcmd  tea.Cmd\n\t\tcmds []tea.Cmd\n\t)\n\n\tswitch msg := msg.(type) {\n\tcase tea.KeyMsg:\n\t\tswitch msg.String() {\n\t\tcase \"ctrl+c\", \"esc\", \"q\":\n\t\t\tcmds = append(cmds, tea.Quit)\n\n\t\tcase \"a\":\n\t\t\tm.tableDefault = m.tableDefault.Focused(true)\n\t\t\tm.tableWithRowIndices = m.tableWithRowIndices.Focused(false)\n\n\t\tcase \"b\":\n\t\t\tm.tableDefault = m.tableDefault.Focused(false)\n\t\t\tm.tableWithRowIndices = m.tableWithRowIndices.Focused(true)\n\n\t\tcase \"u\":\n\t\t\tm.tableDefault = m.tableDefault.WithPageSize(m.tableDefault.PageSize() - 1)\n\t\t\tm.tableWithRowIndices = m.tableWithRowIndices.WithPageSize(m.tableWithRowIndices.PageSize() - 1)\n\n\t\tcase \"i\":\n\t\t\tm.tableDefault = m.tableDefault.WithPageSize(m.tableDefault.PageSize() + 1)\n\t\t\tm.tableWithRowIndices = m.tableWithRowIndices.WithPageSize(m.tableWithRowIndices.PageSize() + 1)\n\n\t\tcase \"r\":\n\t\t\tm.tableDefault = m.tableDefault.WithCurrentPage(rand.Intn(m.tableDefault.MaxPages()) + 1)\n\t\t\tm.tableWithRowIndices = m.tableWithRowIndices.WithCurrentPage(rand.Intn(m.tableWithRowIndices.MaxPages()) + 1)\n\n\t\tcase \"z\":\n\t\t\tif m.rowCount < 10 {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tm.rowCount -= 10\n\t\t\tm.regenTableRows()\n\n\t\tcase \"x\":\n\t\t\tm.rowCount += 10\n\t\t\tm.regenTableRows()\n\t\t}\n\t}\n\n\tm.tableDefault, cmd = m.tableDefault.Update(msg)\n\tcmds = append(cmds, cmd)\n\n\tm.tableWithRowIndices, cmd = m.tableWithRowIndices.Update(msg)\n\tcmds = append(cmds, cmd)\n\n\t// Write a custom footer\n\tstart, end := m.tableWithRowIndices.VisibleIndices()\n\tm.tableWithRowIndices = m.tableWithRowIndices.WithStaticFooter(\n\t\tfmt.Sprintf(\"%d-%d of %d\", start+1, end+1, m.tableWithRowIndices.TotalRows()),\n\t)\n\n\treturn m, tea.Batch(cmds...)\n}\n\nfunc (m Model) View() string {\n\tbody := strings.Builder{}\n\n\tbody.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\")\n\n\tpad := lipgloss.NewStyle().Padding(1)\n\n\ttables := []string{\n\t\tlipgloss.JoinVertical(lipgloss.Center, \"A\", pad.Render(m.tableDefault.View())),\n\t\tlipgloss.JoinVertical(lipgloss.Center, \"B\", pad.Render(m.tableWithRowIndices.View())),\n\t}\n\n\tbody.WriteString(lipgloss.JoinHorizontal(lipgloss.Top, tables...))\n\n\treturn body.String()\n}\n\nfunc main() {\n\tp := tea.NewProgram(NewModel())\n\n\tif err := p.Start(); err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n"
  },
  {
    "path": "examples/pokemon/README.md",
    "content": "# Pokemon example\n\nThis example is a general use of the table using various features to nicely\ndisplay a table of Pokemon and some interesting statistics about them.\n\n![Styled table](https://user-images.githubusercontent.com/5923958/156778142-cc1a32e1-1b1e-4a65-b699-187f39f0f946.png)\n"
  },
  {
    "path": "examples/pokemon/main.go",
    "content": "package main\n\nimport (\n\t\"log\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/charmbracelet/lipgloss\"\n\t\"github.com/evertras/bubble-table/table\"\n)\n\nconst (\n\tcolumnKeyName              = \"name\"\n\tcolumnKeyElement           = \"element\"\n\tcolumnKeyConversations     = \"convos\"\n\tcolumnKeyPositiveSentiment = \"positive\"\n\tcolumnKeyNegativeSentiment = \"negative\"\n\n\tcolorNormal   = \"#fa0\"\n\tcolorElectric = \"#ff0\"\n\tcolorFire     = \"#f64\"\n\tcolorPlant    = \"#8b8\"\n\tcolorWater    = \"#44f\"\n)\n\nvar (\n\tstyleSubtle = lipgloss.NewStyle().Foreground(lipgloss.Color(\"#888\"))\n\n\tstyleBase = lipgloss.NewStyle().\n\t\t\tForeground(lipgloss.Color(\"#a7a\")).\n\t\t\tBorderForeground(lipgloss.Color(\"#a38\")).\n\t\t\tAlign(lipgloss.Right)\n)\n\ntype Model struct {\n\tpokeTable            table.Model\n\tfavoriteElementIndex int\n}\n\nvar elementList = []string{\n\t\"Normal\",\n\t\"Electric\",\n\t\"Fire\",\n\t\"Plant\",\n\t\"Water\",\n}\n\nvar colorMap = map[any]string{\n\t\"Electric\": colorElectric,\n\t\"Fire\":     colorFire,\n\t\"Plant\":    colorPlant,\n\t\"Water\":    colorWater,\n}\n\nfunc makeRow(name, element string, numConversations int, positiveSentiment, negativeSentiment float32) table.Row {\n\telementStyleFunc := func(input table.StyledCellFuncInput) lipgloss.Style {\n\t\tcolor := colorNormal\n\n\t\tif val, ok := colorMap[input.Data]; ok {\n\t\t\tcolor = val\n\t\t}\n\n\t\tstyle := lipgloss.NewStyle().Foreground(lipgloss.Color(color))\n\n\t\tif input.GlobalMetadata[\"favoriteElement\"] == input.Data {\n\t\t\tstyle = style.Italic(true)\n\t\t}\n\n\t\treturn style\n\t}\n\n\treturn table.NewRow(table.RowData{\n\t\tcolumnKeyName:              name,\n\t\tcolumnKeyElement:           table.NewStyledCellWithStyleFunc(element, elementStyleFunc),\n\t\tcolumnKeyConversations:     numConversations,\n\t\tcolumnKeyPositiveSentiment: positiveSentiment,\n\t\tcolumnKeyNegativeSentiment: negativeSentiment,\n\t})\n}\n\nfunc genMetadata(favoriteElementIndex int) map[string]any {\n\treturn map[string]any{\n\t\t\"favoriteElement\": elementList[favoriteElementIndex],\n\t}\n}\n\nfunc NewModel() Model {\n\tinitialFavoriteElementIndex := 0\n\treturn Model{\n\t\tfavoriteElementIndex: initialFavoriteElementIndex,\n\t\tpokeTable: table.New([]table.Column{\n\t\t\ttable.NewColumn(columnKeyName, \"Name\", 13),\n\t\t\ttable.NewColumn(columnKeyElement, \"Element\", 10),\n\t\t\ttable.NewColumn(columnKeyConversations, \"# Conversations\", 15),\n\t\t\ttable.NewColumn(columnKeyPositiveSentiment, \":D %\", 6).\n\t\t\t\tWithStyle(lipgloss.NewStyle().Foreground(lipgloss.Color(\"#8c8\"))).\n\t\t\t\tWithFormatString(\"%.1f%%\"),\n\t\t\ttable.NewColumn(columnKeyNegativeSentiment, \":( %\", 6).\n\t\t\t\tWithStyle(lipgloss.NewStyle().Foreground(lipgloss.Color(\"#c88\"))).\n\t\t\t\tWithFormatString(\"%.1f%%\"),\n\t\t}).WithRows([]table.Row{\n\t\t\tmakeRow(\"Pikachu\", \"Electric\", 2300648, 21.9, 8.54),\n\t\t\tmakeRow(\"Eevee\", \"Normal\", 636373, 26.4, 7.37),\n\t\t\tmakeRow(\"Bulbasaur\", \"Plant\", 352190, 25.7, 9.02),\n\t\t\tmakeRow(\"Squirtle\", \"Water\", 241259, 25.6, 5.96),\n\t\t\tmakeRow(\"Blastoise\", \"Water\", 162794, 19.5, 6.04),\n\t\t\tmakeRow(\"Charmander\", \"Fire\", 265760, 31.2, 5.25),\n\t\t\tmakeRow(\"Charizard\", \"Fire\", 567763, 25.6, 7.56),\n\t\t}).\n\t\t\tBorderRounded().\n\t\t\tWithBaseStyle(styleBase).\n\t\t\tWithPageSize(6).\n\t\t\tSortByDesc(columnKeyConversations).\n\t\t\tFocused(true).\n\t\t\tWithGlobalMetadata(genMetadata(initialFavoriteElementIndex)),\n\t}\n}\n\nfunc (m Model) Init() tea.Cmd {\n\treturn nil\n}\n\nfunc (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {\n\tvar (\n\t\tcmd  tea.Cmd\n\t\tcmds []tea.Cmd\n\t)\n\n\tm.pokeTable, cmd = m.pokeTable.Update(msg)\n\tcmds = append(cmds, cmd)\n\n\tswitch msg := msg.(type) {\n\tcase tea.KeyMsg:\n\t\tswitch msg.String() {\n\t\tcase \"ctrl+c\", \"esc\", \"q\":\n\t\t\tcmds = append(cmds, tea.Quit)\n\n\t\tcase \"e\":\n\t\t\tm.favoriteElementIndex++\n\t\t\tif m.favoriteElementIndex >= len(elementList) {\n\t\t\t\tm.favoriteElementIndex = 0\n\t\t\t}\n\n\t\t\tm.pokeTable = m.pokeTable.WithGlobalMetadata(genMetadata(m.favoriteElementIndex))\n\t\t}\n\t}\n\n\treturn m, tea.Batch(cmds...)\n}\n\nfunc (m Model) View() string {\n\tselected := m.pokeTable.HighlightedRow().Data[columnKeyName].(string)\n\tview := lipgloss.JoinVertical(\n\t\tlipgloss.Left,\n\t\tstyleSubtle.Render(\"Press q or ctrl+c to quit - Sorted by # Conversations\"),\n\t\tstyleSubtle.Render(\"Highlighted: \"+selected),\n\t\tstyleSubtle.Render(\"Favorite element: \"+elementList[m.favoriteElementIndex]),\n\t\tstyleSubtle.Render(\"https://www.nintendolife.com/news/2021/11/these-are-the-most-loved-and-most-hated-pokemon-according-to-a-new-study\"),\n\t\tm.pokeTable.View(),\n\t) + \"\\n\"\n\n\treturn lipgloss.NewStyle().MarginLeft(1).Render(view)\n}\n\nfunc main() {\n\tp := tea.NewProgram(NewModel())\n\n\tif err := p.Start(); err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n"
  },
  {
    "path": "examples/scrolling/README.md",
    "content": "# Scrolling example\n\nThis example shows how to use scrolling to navigate particularly large tables\nthat may not fit nicely onto the screen.\n\n<img width=\"423\" alt=\"image\" src=\"https://user-images.githubusercontent.com/5923958/172004548-7052993e-9e60-44a4-b9b2-b49506c48fb6.png\">\n"
  },
  {
    "path": "examples/scrolling/main.go",
    "content": "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-table/table\"\n)\n\nconst (\n\tcolumnKeyID = \"id\"\n\n\tnumCols = 100\n\tnumRows = 10\n\tidWidth = 5\n\n\tcolWidth = 3\n\tmaxWidth = 30\n)\n\ntype Model struct {\n\tscrollableTable table.Model\n}\n\nfunc colKey(colNum int) string {\n\treturn fmt.Sprintf(\"%d\", colNum)\n}\n\nfunc genRow(id int) table.Row {\n\tdata := table.RowData{\n\t\tcolumnKeyID: fmt.Sprintf(\"ID %d\", id),\n\t}\n\n\tfor i := 0; i < numCols; i++ {\n\t\tdata[colKey(i)] = colWidth\n\t}\n\n\treturn table.NewRow(data)\n}\n\nfunc NewModel() Model {\n\trows := []table.Row{}\n\n\tfor i := 0; i < numRows; i++ {\n\t\trows = append(rows, genRow(i))\n\t}\n\n\tcols := []table.Column{\n\t\ttable.NewColumn(columnKeyID, \"ID\", idWidth),\n\t}\n\n\tfor i := 0; i < numCols; i++ {\n\t\tcols = append(cols, table.NewColumn(colKey(i), colKey(i+1), colWidth))\n\t}\n\n\tt := table.New(cols).\n\t\tWithRows(rows).\n\t\tWithMaxTotalWidth(maxWidth).\n\t\tWithHorizontalFreezeColumnCount(1).\n\t\tWithStaticFooter(\"A footer\").\n\t\tFocused(true)\n\n\treturn Model{\n\t\tscrollableTable: t,\n\t}\n}\n\nfunc (m Model) Init() tea.Cmd {\n\treturn nil\n}\n\nfunc (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {\n\tvar (\n\t\tcmd  tea.Cmd\n\t\tcmds []tea.Cmd\n\t)\n\n\tm.scrollableTable, cmd = m.scrollableTable.Update(msg)\n\tcmds = append(cmds, cmd)\n\n\tswitch msg := msg.(type) {\n\tcase tea.KeyMsg:\n\t\tswitch msg.String() {\n\t\tcase \"ctrl+c\", \"esc\", \"q\":\n\t\t\tcmds = append(cmds, tea.Quit)\n\t\t}\n\t}\n\n\treturn m, tea.Batch(cmds...)\n}\n\nfunc (m Model) View() string {\n\tbody := strings.Builder{}\n\n\tbody.WriteString(\"A scrollable table\\nPress shift+left or shift+right to scroll\\nPress q or ctrl+c to quit\\n\\n\")\n\n\tbody.WriteString(m.scrollableTable.View())\n\n\treturn body.String()\n}\n\nfunc main() {\n\tp := tea.NewProgram(NewModel())\n\n\tif err := p.Start(); err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n"
  },
  {
    "path": "examples/simplest/README.md",
    "content": "# Simplest example\n\nThis is a bare bones example of how to get started with the table component.  It\nuses all the defaults to simply present data, and is a good starting point for\nother use cases.\n\n<img width=\"462\" alt=\"image\" src=\"https://user-images.githubusercontent.com/5923958/170802181-f0c54b8b-625a-4ff0-8ffa-0e36cc2567eb.png\">\n"
  },
  {
    "path": "examples/simplest/main.go",
    "content": "package main\n\nimport (\n\t\"log\"\n\t\"strings\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/evertras/bubble-table/table\"\n)\n\nconst (\n\tcolumnKeyName    = \"name\"\n\tcolumnKeyElement = \"element\"\n)\n\ntype Model struct {\n\tsimpleTable table.Model\n}\n\nfunc NewModel() Model {\n\treturn Model{\n\t\tsimpleTable: table.New([]table.Column{\n\t\t\ttable.NewColumn(columnKeyName, \"Name\", 13),\n\t\t\ttable.NewColumn(columnKeyElement, \"Element\", 10),\n\t\t}).WithRows([]table.Row{\n\t\t\ttable.NewRow(table.RowData{\n\t\t\t\tcolumnKeyName:    \"Pikachu\",\n\t\t\t\tcolumnKeyElement: \"Electric\",\n\t\t\t}),\n\t\t\ttable.NewRow(table.RowData{\n\t\t\t\tcolumnKeyName:    \"Charmander\",\n\t\t\t\tcolumnKeyElement: \"Fire\",\n\t\t\t}),\n\t\t}),\n\t}\n}\n\nfunc (m Model) Init() tea.Cmd {\n\treturn nil\n}\n\nfunc (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {\n\tvar (\n\t\tcmd  tea.Cmd\n\t\tcmds []tea.Cmd\n\t)\n\n\tm.simpleTable, cmd = m.simpleTable.Update(msg)\n\tcmds = append(cmds, cmd)\n\n\tswitch msg := msg.(type) {\n\tcase tea.KeyMsg:\n\t\tswitch msg.String() {\n\t\tcase \"ctrl+c\", \"esc\", \"q\":\n\t\t\tcmds = append(cmds, tea.Quit)\n\t\t}\n\t}\n\n\treturn m, tea.Batch(cmds...)\n}\n\nfunc (m Model) View() string {\n\tbody := strings.Builder{}\n\n\tbody.WriteString(\"A very simple default table (non-interactive)\\nPress q or ctrl+c to quit\\n\\n\")\n\n\tbody.WriteString(m.simpleTable.View())\n\n\treturn body.String()\n}\n\nfunc main() {\n\tp := tea.NewProgram(NewModel())\n\n\tif err := p.Start(); err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n"
  },
  {
    "path": "examples/sorting/README.md",
    "content": "# Sorting example\n\nThis example shows how to use the sorting feature, which sorts columns.  It\ndemonstrates how numbers are numerically sorted while strings are alphabetically\nsorted.  It also demonstrates multi-column sorting to group different kinds of\ndata together and sort within them.\n\n<img width=\"461\" alt=\"image\" src=\"https://user-images.githubusercontent.com/5923958/187064825-59d60dc7-cc75-4c24-bc3a-d653decdf700.png\">\n"
  },
  {
    "path": "examples/sorting/main.go",
    "content": "package main\n\nimport (\n\t\"log\"\n\t\"strings\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/evertras/bubble-table/table\"\n)\n\nconst (\n\tcolumnKeyName = \"name\"\n\tcolumnKeyType = \"type\"\n\tcolumnKeyWins = \"wins\"\n)\n\ntype Model struct {\n\tsimpleTable table.Model\n\n\tcolumnSortKey string\n\tsortDirection string\n}\n\nfunc NewModel() Model {\n\treturn Model{\n\t\tsimpleTable: table.New([]table.Column{\n\t\t\ttable.NewColumn(columnKeyName, \"Name\", 13),\n\t\t\ttable.NewColumn(columnKeyType, \"Type\", 13),\n\t\t\ttable.NewColumn(columnKeyWins, \"Win %\", 8).\n\t\t\t\tWithFormatString(\"%.1f%%\"),\n\t\t}).WithRows([]table.Row{\n\t\t\ttable.NewRow(table.RowData{\n\t\t\t\tcolumnKeyName: \"ピカピカ\",\n\t\t\t\tcolumnKeyType: \"Pikachu\",\n\t\t\t\tcolumnKeyWins: 78.3,\n\t\t\t}),\n\t\t\ttable.NewRow(table.RowData{\n\t\t\t\tcolumnKeyName: \"Zapmouse\",\n\t\t\t\tcolumnKeyType: \"Pikachu\",\n\t\t\t\tcolumnKeyWins: 3.3,\n\t\t\t}),\n\t\t\ttable.NewRow(table.RowData{\n\t\t\t\tcolumnKeyName: \"Burninator\",\n\t\t\t\tcolumnKeyType: \"Charmander\",\n\t\t\t\tcolumnKeyWins: 32.1,\n\t\t\t}),\n\t\t\ttable.NewRow(table.RowData{\n\t\t\t\tcolumnKeyName: \"Alphonse\",\n\t\t\t\tcolumnKeyType: \"Pikachu\",\n\t\t\t\tcolumnKeyWins: 13.8,\n\t\t\t}),\n\t\t\ttable.NewRow(table.RowData{\n\t\t\t\tcolumnKeyName: \"Trogdor\",\n\t\t\t\tcolumnKeyType: \"Charmander\",\n\t\t\t\tcolumnKeyWins: 99.9,\n\t\t\t}),\n\t\t\ttable.NewRow(table.RowData{\n\t\t\t\tcolumnKeyName: \"Dihydrogen Monoxide\",\n\t\t\t\tcolumnKeyType: \"Squirtle\",\n\t\t\t\tcolumnKeyWins: 31.348,\n\t\t\t}),\n\t\t}),\n\t}\n}\n\nfunc (m Model) Init() tea.Cmd {\n\treturn nil\n}\n\nfunc (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {\n\tvar (\n\t\tcmd  tea.Cmd\n\t\tcmds []tea.Cmd\n\t)\n\n\tm.simpleTable, cmd = m.simpleTable.Update(msg)\n\tcmds = append(cmds, cmd)\n\n\tswitch msg := msg.(type) {\n\tcase tea.KeyMsg:\n\t\tswitch msg.String() {\n\t\tcase \"ctrl+c\", \"esc\", \"q\":\n\t\t\tcmds = append(cmds, tea.Quit)\n\n\t\tcase \"n\":\n\t\t\tm.columnSortKey = columnKeyName\n\t\t\tm.simpleTable = m.simpleTable.SortByAsc(m.columnSortKey)\n\n\t\tcase \"t\":\n\t\t\tm.columnSortKey = columnKeyType\n\t\t\t// Within the same type, order each by wins\n\t\t\tm.simpleTable = m.simpleTable.SortByAsc(m.columnSortKey).ThenSortByDesc(columnKeyWins)\n\n\t\tcase \"w\":\n\t\t\tm.columnSortKey = columnKeyWins\n\t\t\tm.simpleTable = m.simpleTable.SortByDesc(m.columnSortKey)\n\t\t}\n\t}\n\n\treturn m, tea.Batch(cmds...)\n}\n\nfunc (m Model) View() string {\n\tbody := strings.Builder{}\n\n\tbody.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\")\n\n\tbody.WriteString(m.simpleTable.View())\n\n\treturn body.String()\n}\n\nfunc main() {\n\tp := tea.NewProgram(NewModel())\n\n\tif err := p.Start(); err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n"
  },
  {
    "path": "examples/updates/README.md",
    "content": "# Update example\n\nShows how to update data in the table from an external API returning data.\n\n![table-gif](https://user-images.githubusercontent.com/5923958/170802479-a4395407-f286-42d6-9165-0580db6030db.gif)\n"
  },
  {
    "path": "examples/updates/data.go",
    "content": "package main\n\nimport \"math/rand\"\n\n// SomeData represent some real data of some sort, unaware of tables\ntype SomeData struct {\n\tID     string\n\tScore  int\n\tStatus string\n}\n\n// NewSomeData creates SomeData that has an ID and randomized values\nfunc NewSomeData(id string) *SomeData {\n\ts := &SomeData{\n\t\tID: id,\n\t}\n\n\t// Start with some random data\n\ts.RandomizeScoreAndStatus()\n\n\treturn s\n}\n\n// RandomizeScoreAndStatus does an in-place update to simulate some data being\n// updated by some other process\nfunc (s *SomeData) RandomizeScoreAndStatus() {\n\ts.Score = rand.Intn(100) + 1\n\n\tif s.Score < 30 {\n\t\ts.Status = \"Critical\"\n\t} else if s.Score < 80 {\n\t\ts.Status = \"Stable\"\n\t} else {\n\t\ts.Status = \"Good\"\n\t}\n}\n"
  },
  {
    "path": "examples/updates/main.go",
    "content": "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/charmbracelet/lipgloss\"\n\t\"github.com/evertras/bubble-table/table\"\n)\n\nconst (\n\tcolumnKeyID     = \"id\"\n\tcolumnKeyScore  = \"score\"\n\tcolumnKeyStatus = \"status\"\n)\n\nvar (\n\tstyleCritical = lipgloss.NewStyle().Foreground(lipgloss.Color(\"#f00\"))\n\tstyleStable   = lipgloss.NewStyle().Foreground(lipgloss.Color(\"#ff0\"))\n\tstyleGood     = lipgloss.NewStyle().Foreground(lipgloss.Color(\"#0f0\"))\n)\n\ntype Model struct {\n\ttable table.Model\n\n\tupdateDelay time.Duration\n\n\tdata []*SomeData\n}\n\nfunc rowStyleFunc(input table.RowStyleFuncInput) lipgloss.Style {\n\tcalculatedStyle := lipgloss.NewStyle()\n\n\tswitch input.Row.Data[columnKeyStatus] {\n\tcase \"Critical\":\n\t\tcalculatedStyle = styleCritical.Copy()\n\tcase \"Stable\":\n\t\tcalculatedStyle = styleStable.Copy()\n\tcase \"Good\":\n\t\tcalculatedStyle = styleGood.Copy()\n\t}\n\n\tif input.Index%2 == 0 {\n\t\tcalculatedStyle = calculatedStyle.Background(lipgloss.Color(\"#222\"))\n\t} else {\n\t\tcalculatedStyle = calculatedStyle.Background(lipgloss.Color(\"#444\"))\n\t}\n\n\treturn calculatedStyle\n}\n\nfunc NewModel() Model {\n\treturn Model{\n\t\ttable:       table.New(generateColumns(0)).WithRowStyleFunc(rowStyleFunc),\n\t\tupdateDelay: time.Second,\n\t}\n}\n\n// This data is stored somewhere else, maybe on a client or some other thing\nfunc refreshDataCmd() tea.Msg {\n\t// This could come from some API or something\n\treturn []*SomeData{\n\t\tNewSomeData(\"abc\"),\n\t\tNewSomeData(\"def\"),\n\t\tNewSomeData(\"123\"),\n\t\tNewSomeData(\"ok\"),\n\t\tNewSomeData(\"another\"),\n\t\tNewSomeData(\"yay\"),\n\t\tNewSomeData(\"more\"),\n\t}\n}\n\n// Generate columns based on how many are critical to show some summary\nfunc generateColumns(numCritical int) []table.Column {\n\t// Show how many critical there are\n\tstatusStr := fmt.Sprintf(\"Score (%d)\", numCritical)\n\tstatusCol := table.NewColumn(columnKeyStatus, statusStr, 10)\n\n\tif numCritical > 3 {\n\t\t// This normally applies the critical style to everything in the column,\n\t\t// but in this case we apply a row style which overrides it anyway.\n\t\tstatusCol = statusCol.WithStyle(styleCritical)\n\t}\n\n\treturn []table.Column{\n\t\ttable.NewColumn(columnKeyID, \"ID\", 10),\n\t\ttable.NewColumn(columnKeyScore, \"Score\", 8),\n\t\tstatusCol,\n\t}\n}\n\nfunc (m Model) Init() tea.Cmd {\n\treturn refreshDataCmd\n}\n\nfunc (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {\n\tvar (\n\t\tcmd  tea.Cmd\n\t\tcmds []tea.Cmd\n\t)\n\n\tm.table, cmd = m.table.Update(msg)\n\tcmds = append(cmds, cmd)\n\n\tswitch msg := msg.(type) {\n\tcase tea.KeyMsg:\n\t\tswitch msg.String() {\n\t\tcase \"ctrl+c\", \"esc\", \"q\":\n\t\t\tcmds = append(cmds, tea.Quit)\n\n\t\tcase \"up\":\n\t\t\tif m.updateDelay < time.Second {\n\t\t\t\tm.updateDelay *= 10\n\t\t\t}\n\n\t\tcase \"down\":\n\t\t\tif m.updateDelay > time.Millisecond*1 {\n\t\t\t\tm.updateDelay /= 10\n\t\t\t}\n\t\t}\n\n\tcase []*SomeData:\n\t\tm.data = msg\n\n\t\tnumCritical := 0\n\n\t\tfor _, d := range msg {\n\t\t\tif d.Status == \"Critical\" {\n\t\t\t\tnumCritical++\n\t\t\t}\n\t\t}\n\n\t\t// Reapply the new data and the new columns based on critical count\n\t\tm.table = m.table.WithRows(generateRowsFromData(m.data)).WithColumns(generateColumns(numCritical))\n\n\t\t// This can be from any source, but for demo purposes let's party!\n\t\tdelay := m.updateDelay\n\t\tcmds = append(cmds, func() tea.Msg {\n\t\t\ttime.Sleep(delay)\n\t\t\treturn refreshDataCmd()\n\t\t})\n\t}\n\n\treturn m, tea.Batch(cmds...)\n}\n\nfunc (m Model) View() string {\n\tbody := strings.Builder{}\n\n\tbody.WriteString(\n\t\tfmt.Sprintf(\n\t\t\t\"Table demo with updating data!  Updating every %v\\nPress up/down to update faster/slower\\nPress q or ctrl+c to quit\\n\",\n\t\t\tm.updateDelay,\n\t\t))\n\n\tpad := lipgloss.NewStyle().Padding(1)\n\n\tbody.WriteString(pad.Render(m.table.View()))\n\n\treturn body.String()\n}\n\nfunc generateRowsFromData(data []*SomeData) []table.Row {\n\trows := []table.Row{}\n\n\tfor _, entry := range data {\n\t\trow := table.NewRow(table.RowData{\n\t\t\tcolumnKeyID:     entry.ID,\n\t\t\tcolumnKeyScore:  entry.Score,\n\t\t\tcolumnKeyStatus: entry.Status,\n\t\t})\n\n\t\trows = append(rows, row)\n\t}\n\n\treturn rows\n}\n\nfunc main() {\n\n\tp := tea.NewProgram(NewModel())\n\n\tif err := p.Start(); err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n"
  },
  {
    "path": "flake.nix",
    "content": "{\n  inputs = {\n    nixpkgs.url = \"nixpkgs/nixos-23.11\";\n    flake-utils.url = \"github:numtide/flake-utils\";\n  };\n\n  outputs = { self, nixpkgs, flake-utils }:\n    flake-utils.lib.eachDefaultSystem (system:\n      let\n        pkgs = nixpkgs.legacyPackages.${system};\n      in\n      {\n        devShells.default = pkgs.mkShell {\n          packages = with pkgs; [\n            # Dev tools\n            go\n          ];\n        };\n      });\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/evertras/bubble-table\n\ngo 1.18\n\nrequire (\n\tgithub.com/charmbracelet/bubbles v0.11.0\n\tgithub.com/charmbracelet/bubbletea v0.21.0\n\tgithub.com/charmbracelet/lipgloss v0.5.0\n\tgithub.com/mattn/go-runewidth v0.0.13\n\tgithub.com/muesli/reflow v0.3.0\n\tgithub.com/stretchr/testify v1.7.0\n)\n\nrequire (\n\tgithub.com/atotto/clipboard v0.1.4 // indirect\n\tgithub.com/containerd/console v1.0.3 // indirect\n\tgithub.com/davecgh/go-spew v1.1.0 // indirect\n\tgithub.com/lucasb-eyer/go-colorful v1.2.0 // indirect\n\tgithub.com/mattn/go-isatty v0.0.14 // indirect\n\tgithub.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect\n\tgithub.com/muesli/cancelreader v0.2.0 // indirect\n\tgithub.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.0 // indirect\n\tgithub.com/rivo/uniseg v0.2.0 // indirect\n\tgolang.org/x/sys v0.0.0-20220209214540-3681064d5158 // indirect\n\tgolang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect\n\tgopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=\ngithub.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=\ngithub.com/charmbracelet/bubbles v0.11.0 h1:fBLyY0PvJnd56Vlu5L84JJH6f4axhgIJ9P3NET78f0Q=\ngithub.com/charmbracelet/bubbles v0.11.0/go.mod h1:bbeTiXwPww4M031aGi8UK2HT9RDWoiNibae+1yCMtcc=\ngithub.com/charmbracelet/bubbletea v0.21.0 h1:f3y+kanzgev5PA916qxmDybSHU3N804uOnKnhRPXTcI=\ngithub.com/charmbracelet/bubbletea v0.21.0/go.mod h1:GgmJMec61d08zXsOhqRC/AiOx4K4pmz+VIcRIm1FKr4=\ngithub.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=\ngithub.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DATR2/8t8=\ngithub.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs=\ngithub.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=\ngithub.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=\ngithub.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=\ngithub.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=\ngithub.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=\ngithub.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=\ngithub.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=\ngithub.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=\ngithub.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=\ngithub.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=\ngithub.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=\ngithub.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34=\ngithub.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=\ngithub.com/muesli/cancelreader v0.2.0 h1:SOpr+CfyVNce341kKqvbhhzQhBPyJRXQaCtn03Pae1Q=\ngithub.com/muesli/cancelreader v0.2.0/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=\ngithub.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=\ngithub.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=\ngithub.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=\ngithub.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=\ngithub.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 h1:QANkGiGr39l1EESqrE0gZw0/AJNYzIvoGLhIoVYtluI=\ngithub.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=\ngithub.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=\ngithub.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=\ngithub.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngolang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220209214540-3681064d5158 h1:rm+CHSpPEEW2IsXUib1ThaHIjuBVZjxNgSKmBLFfD4c=\ngolang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "table/benchmarks_test.go",
    "content": "package table\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n)\n\nvar benchView string\n\nfunc benchTable(numColumns, numDataRows int) Model {\n\tcolumns := []Column{}\n\n\tfor i := 0; i < numColumns; i++ {\n\t\tiStr := fmt.Sprintf(\"%d\", i)\n\t\tcolumns = append(columns, NewColumn(iStr, iStr, 6))\n\t}\n\n\trows := []Row{}\n\n\tfor i := 0; i < numDataRows; i++ {\n\t\trowData := RowData{}\n\n\t\tfor columnIndex, column := range columns {\n\t\t\trowData[column.key] = fmt.Sprintf(\"%d\", columnIndex)\n\t\t}\n\n\t\trows = append(rows, NewRow(rowData))\n\t}\n\n\treturn New(columns).WithRows(rows)\n}\n\nfunc BenchmarkPlain3x3TableView(b *testing.B) {\n\tmakeRow := func(id, name string, score int) Row {\n\t\treturn NewRow(RowData{\n\t\t\t\"id\":    id,\n\t\t\t\"name\":  name,\n\t\t\t\"score\": score,\n\t\t})\n\t}\n\n\tmodel := New([]Column{\n\t\tNewColumn(\"id\", \"ID\", 3),\n\t\tNewColumn(\"name\", \"Name\", 8),\n\t\tNewColumn(\"score\", \"Score\", 6),\n\t}).WithRows([]Row{\n\t\tmakeRow(\"abc\", \"First\", 17),\n\t\tmakeRow(\"def\", \"Second\", 1034),\n\t\tmakeRow(\"123\", \"Third\", 841),\n\t})\n\n\tb.ResetTimer()\n\n\tfor n := 0; n < b.N; n++ {\n\t\tbenchView = model.View()\n\t}\n}\n\nfunc BenchmarkPlainTableViews(b *testing.B) {\n\tsizes := []struct {\n\t\tnumColumns int\n\t\tnumRows    int\n\t}{\n\t\t{\n\t\t\tnumColumns: 1,\n\t\t\tnumRows:    0,\n\t\t},\n\t\t{\n\t\t\tnumColumns: 10,\n\t\t\tnumRows:    0,\n\t\t},\n\t\t{\n\t\t\tnumColumns: 1,\n\t\t\tnumRows:    4,\n\t\t},\n\t\t{\n\t\t\tnumColumns: 1,\n\t\t\tnumRows:    19,\n\t\t},\n\t\t{\n\t\t\tnumColumns: 9,\n\t\t\tnumRows:    19,\n\t\t},\n\t\t{\n\t\t\tnumColumns: 9,\n\t\t\tnumRows:    49,\n\t\t},\n\t}\n\n\tfor _, size := range sizes {\n\t\tb.Run(fmt.Sprintf(\"%dx%d\", size.numColumns, size.numRows+1), func(b *testing.B) {\n\t\t\tmodel := benchTable(size.numColumns, size.numRows)\n\t\t\tb.ResetTimer()\n\n\t\t\tfor n := 0; n < b.N; n++ {\n\t\t\t\tbenchView = model.View()\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "table/border.go",
    "content": "package table\n\nimport \"github.com/charmbracelet/lipgloss\"\n\n// Border defines the borders in and around the table.\ntype Border struct {\n\tTop         string\n\tLeft        string\n\tRight       string\n\tBottom      string\n\tTopRight    string\n\tTopLeft     string\n\tBottomRight string\n\tBottomLeft  string\n\n\tTopJunction    string\n\tLeftJunction   string\n\tRightJunction  string\n\tBottomJunction string\n\n\tInnerJunction string\n\n\tInnerDivider string\n\n\t// Styles for 2x2 tables and larger\n\tstyleMultiTopLeft     lipgloss.Style\n\tstyleMultiTop         lipgloss.Style\n\tstyleMultiTopRight    lipgloss.Style\n\tstyleMultiRight       lipgloss.Style\n\tstyleMultiBottomRight lipgloss.Style\n\tstyleMultiBottom      lipgloss.Style\n\tstyleMultiBottomLeft  lipgloss.Style\n\tstyleMultiLeft        lipgloss.Style\n\tstyleMultiInner       lipgloss.Style\n\n\t// Styles for a single column table\n\tstyleSingleColumnTop    lipgloss.Style\n\tstyleSingleColumnInner  lipgloss.Style\n\tstyleSingleColumnBottom lipgloss.Style\n\n\t// Styles for a single row table\n\tstyleSingleRowLeft  lipgloss.Style\n\tstyleSingleRowInner lipgloss.Style\n\tstyleSingleRowRight lipgloss.Style\n\n\t// Style for a table with only one cell\n\tstyleSingleCell lipgloss.Style\n\n\t// Style for the footer\n\tstyleFooter lipgloss.Style\n}\n\nvar (\n\t// https://www.w3.org/TR/xml-entity-names/025.html\n\n\tborderDefault = Border{\n\t\tTop:    \"━\",\n\t\tLeft:   \"┃\",\n\t\tRight:  \"┃\",\n\t\tBottom: \"━\",\n\n\t\tTopRight:    \"┓\",\n\t\tTopLeft:     \"┏\",\n\t\tBottomRight: \"┛\",\n\t\tBottomLeft:  \"┗\",\n\n\t\tTopJunction:    \"┳\",\n\t\tLeftJunction:   \"┣\",\n\t\tRightJunction:  \"┫\",\n\t\tBottomJunction: \"┻\",\n\t\tInnerJunction:  \"╋\",\n\n\t\tInnerDivider: \"┃\",\n\t}\n\n\tborderRounded = Border{\n\t\tTop:    \"─\",\n\t\tLeft:   \"│\",\n\t\tRight:  \"│\",\n\t\tBottom: \"─\",\n\n\t\tTopRight:    \"╮\",\n\t\tTopLeft:     \"╭\",\n\t\tBottomRight: \"╯\",\n\t\tBottomLeft:  \"╰\",\n\n\t\tTopJunction:    \"┬\",\n\t\tLeftJunction:   \"├\",\n\t\tRightJunction:  \"┤\",\n\t\tBottomJunction: \"┴\",\n\t\tInnerJunction:  \"┼\",\n\n\t\tInnerDivider: \"│\",\n\t}\n)\n\nfunc init() {\n\tborderDefault.generateStyles()\n\tborderRounded.generateStyles()\n}\n\nfunc (b *Border) generateStyles() {\n\tb.generateMultiStyles()\n\tb.generateSingleColumnStyles()\n\tb.generateSingleRowStyles()\n\tb.generateSingleCellStyle()\n\n\t// The footer is a single cell with the top taken off... usually.  We can\n\t// re-enable the top if needed this way for certain format configurations.\n\tb.styleFooter = b.styleSingleCell.Copy().\n\t\tAlign(lipgloss.Right).\n\t\tBorderBottom(true).\n\t\tBorderRight(true).\n\t\tBorderLeft(true)\n}\n\nfunc (b *Border) styleLeftWithFooter(original lipgloss.Style) lipgloss.Style {\n\tborder := original.GetBorderStyle()\n\n\tborder.BottomLeft = b.LeftJunction\n\n\treturn original.Copy().BorderStyle(border)\n}\n\nfunc (b *Border) styleRightWithFooter(original lipgloss.Style) lipgloss.Style {\n\tborder := original.GetBorderStyle()\n\n\tborder.BottomRight = b.RightJunction\n\n\treturn original.Copy().BorderStyle(border)\n}\n\nfunc (b *Border) styleBothWithFooter(original lipgloss.Style) lipgloss.Style {\n\tborder := original.GetBorderStyle()\n\n\tborder.BottomLeft = b.LeftJunction\n\tborder.BottomRight = b.RightJunction\n\n\treturn original.Copy().BorderStyle(border)\n}\n\n// This function is long, but it's just repetitive...\n//\n//nolint:funlen\nfunc (b *Border) generateMultiStyles() {\n\tb.styleMultiTopLeft = lipgloss.NewStyle().BorderStyle(\n\t\tlipgloss.Border{\n\t\t\tTopLeft:     b.TopLeft,\n\t\t\tTop:         b.Top,\n\t\t\tTopRight:    b.TopJunction,\n\t\t\tRight:       b.InnerDivider,\n\t\t\tBottomRight: b.InnerJunction,\n\t\t\tBottom:      b.Bottom,\n\t\t\tBottomLeft:  b.LeftJunction,\n\t\t\tLeft:        b.Left,\n\t\t},\n\t)\n\n\tb.styleMultiTop = lipgloss.NewStyle().BorderStyle(\n\t\tlipgloss.Border{\n\t\t\tTop:    b.Top,\n\t\t\tRight:  b.InnerDivider,\n\t\t\tBottom: b.Bottom,\n\n\t\t\tTopRight:    b.TopJunction,\n\t\t\tBottomRight: b.InnerJunction,\n\t\t},\n\t).BorderTop(true).BorderBottom(true).BorderRight(true)\n\n\tb.styleMultiTopRight = lipgloss.NewStyle().BorderStyle(\n\t\tlipgloss.Border{\n\t\t\tTop:    b.Top,\n\t\t\tRight:  b.Right,\n\t\t\tBottom: b.Bottom,\n\n\t\t\tTopRight:    b.TopRight,\n\t\t\tBottomRight: b.RightJunction,\n\t\t},\n\t).BorderTop(true).BorderBottom(true).BorderRight(true)\n\n\tb.styleMultiLeft = lipgloss.NewStyle().BorderStyle(\n\t\tlipgloss.Border{\n\t\t\tLeft:  b.Left,\n\t\t\tRight: b.InnerDivider,\n\t\t},\n\t).BorderRight(true).BorderLeft(true)\n\n\tb.styleMultiRight = lipgloss.NewStyle().BorderStyle(\n\t\tlipgloss.Border{\n\t\t\tRight: b.Right,\n\t\t},\n\t).BorderRight(true)\n\n\tb.styleMultiInner = lipgloss.NewStyle().BorderStyle(\n\t\tlipgloss.Border{\n\t\t\tRight: b.InnerDivider,\n\t\t},\n\t).BorderRight(true)\n\n\tb.styleMultiBottomLeft = lipgloss.NewStyle().BorderStyle(\n\t\tlipgloss.Border{\n\t\t\tLeft:   b.Left,\n\t\t\tRight:  b.InnerDivider,\n\t\t\tBottom: b.Bottom,\n\n\t\t\tBottomLeft:  b.BottomLeft,\n\t\t\tBottomRight: b.BottomJunction,\n\t\t},\n\t).BorderLeft(true).BorderBottom(true).BorderRight(true)\n\n\tb.styleMultiBottom = lipgloss.NewStyle().BorderStyle(\n\t\tlipgloss.Border{\n\t\t\tRight:  b.InnerDivider,\n\t\t\tBottom: b.Bottom,\n\n\t\t\tBottomRight: b.BottomJunction,\n\t\t},\n\t).BorderBottom(true).BorderRight(true)\n\n\tb.styleMultiBottomRight = lipgloss.NewStyle().BorderStyle(\n\t\tlipgloss.Border{\n\t\t\tRight:  b.Right,\n\t\t\tBottom: b.Bottom,\n\n\t\t\tBottomRight: b.BottomRight,\n\t\t},\n\t).BorderBottom(true).BorderRight(true)\n}\n\nfunc (b *Border) generateSingleColumnStyles() {\n\tb.styleSingleColumnTop = lipgloss.NewStyle().BorderStyle(\n\t\tlipgloss.Border{\n\t\t\tTop:    b.Top,\n\t\t\tLeft:   b.Left,\n\t\t\tRight:  b.Right,\n\t\t\tBottom: b.Bottom,\n\n\t\t\tTopLeft:     b.TopLeft,\n\t\t\tTopRight:    b.TopRight,\n\t\t\tBottomLeft:  b.LeftJunction,\n\t\t\tBottomRight: b.RightJunction,\n\t\t},\n\t)\n\n\tb.styleSingleColumnInner = lipgloss.NewStyle().BorderStyle(\n\t\tlipgloss.Border{\n\t\t\tLeft:  b.Left,\n\t\t\tRight: b.Right,\n\t\t},\n\t).BorderRight(true).BorderLeft(true)\n\n\tb.styleSingleColumnBottom = lipgloss.NewStyle().BorderStyle(\n\t\tlipgloss.Border{\n\t\t\tLeft:   b.Left,\n\t\t\tRight:  b.Right,\n\t\t\tBottom: b.Bottom,\n\n\t\t\tBottomLeft:  b.BottomLeft,\n\t\t\tBottomRight: b.BottomRight,\n\t\t},\n\t).BorderRight(true).BorderLeft(true).BorderBottom(true)\n}\n\nfunc (b *Border) generateSingleRowStyles() {\n\tb.styleSingleRowLeft = lipgloss.NewStyle().BorderStyle(\n\t\tlipgloss.Border{\n\t\t\tTop:    b.Top,\n\t\t\tLeft:   b.Left,\n\t\t\tRight:  b.InnerDivider,\n\t\t\tBottom: b.Bottom,\n\n\t\t\tBottomLeft:  b.BottomLeft,\n\t\t\tBottomRight: b.BottomJunction,\n\t\t\tTopRight:    b.TopJunction,\n\t\t\tTopLeft:     b.TopLeft,\n\t\t},\n\t)\n\n\tb.styleSingleRowInner = lipgloss.NewStyle().BorderStyle(\n\t\tlipgloss.Border{\n\t\t\tTop:    b.Top,\n\t\t\tRight:  b.InnerDivider,\n\t\t\tBottom: b.Bottom,\n\n\t\t\tBottomRight: b.BottomJunction,\n\t\t\tTopRight:    b.TopJunction,\n\t\t},\n\t).BorderTop(true).BorderBottom(true).BorderRight(true)\n\n\tb.styleSingleRowRight = lipgloss.NewStyle().BorderStyle(\n\t\tlipgloss.Border{\n\t\t\tTop:    b.Top,\n\t\t\tRight:  b.Right,\n\t\t\tBottom: b.Bottom,\n\n\t\t\tBottomRight: b.BottomRight,\n\t\t\tTopRight:    b.TopRight,\n\t\t},\n\t).BorderTop(true).BorderBottom(true).BorderRight(true)\n}\n\nfunc (b *Border) generateSingleCellStyle() {\n\tb.styleSingleCell = lipgloss.NewStyle().BorderStyle(\n\t\tlipgloss.Border{\n\t\t\tTop:    b.Top,\n\t\t\tLeft:   b.Left,\n\t\t\tRight:  b.Right,\n\t\t\tBottom: b.Bottom,\n\n\t\t\tBottomLeft:  b.BottomLeft,\n\t\t\tBottomRight: b.BottomRight,\n\t\t\tTopRight:    b.TopRight,\n\t\t\tTopLeft:     b.TopLeft,\n\t\t},\n\t)\n}\n\n// BorderDefault uses the basic square border, useful to reset the border if\n// it was changed somehow.\nfunc (m Model) BorderDefault() Model {\n\t// Already generated styles\n\tm.border = borderDefault\n\n\treturn m\n}\n\n// BorderRounded uses a thin, rounded border.\nfunc (m Model) BorderRounded() Model {\n\t// Already generated styles\n\tm.border = borderRounded\n\n\treturn m\n}\n\n// Border uses the given border components to render the table.\nfunc (m Model) Border(border Border) Model {\n\tborder.generateStyles()\n\n\tm.border = border\n\n\treturn m\n}\n\ntype borderStyleRow struct {\n\tleft  lipgloss.Style\n\tinner lipgloss.Style\n\tright lipgloss.Style\n}\n\nfunc (b *borderStyleRow) inherit(s lipgloss.Style) {\n\tb.left = b.left.Copy().Inherit(s)\n\tb.inner = b.inner.Copy().Inherit(s)\n\tb.right = b.right.Copy().Inherit(s)\n}\n\n// There's a lot of branches here, but splitting it up further would make it\n// harder to follow. So just be careful with comments and make sure it's tested!\n//\n//nolint:nestif\nfunc (m Model) styleHeaders() borderStyleRow {\n\thasRows := len(m.GetVisibleRows()) > 0 || m.calculatePadding(0) > 0\n\tsingleColumn := len(m.columns) == 1\n\tstyles := borderStyleRow{}\n\n\t// Possible configurations:\n\t// - Single cell\n\t// - Single row\n\t// - Single column\n\t// - Multi\n\n\tif singleColumn {\n\t\tif hasRows {\n\t\t\t// Single column\n\t\t\tstyles.left = m.border.styleSingleColumnTop\n\t\t\tstyles.inner = styles.left\n\t\t\tstyles.right = styles.left\n\t\t} else {\n\t\t\t// Single cell\n\t\t\tstyles.left = m.border.styleSingleCell\n\t\t\tstyles.inner = styles.left\n\t\t\tstyles.right = styles.left\n\n\t\t\tif m.hasFooter() {\n\t\t\t\tstyles.left = m.border.styleBothWithFooter(styles.left)\n\t\t\t}\n\t\t}\n\t} else if !hasRows {\n\t\t// Single row\n\t\tstyles.left = m.border.styleSingleRowLeft\n\t\tstyles.inner = m.border.styleSingleRowInner\n\t\tstyles.right = m.border.styleSingleRowRight\n\n\t\tif m.hasFooter() {\n\t\t\tstyles.left = m.border.styleLeftWithFooter(styles.left)\n\t\t\tstyles.right = m.border.styleRightWithFooter(styles.right)\n\t\t}\n\t} else {\n\t\t// Multi\n\t\tstyles.left = m.border.styleMultiTopLeft\n\t\tstyles.inner = m.border.styleMultiTop\n\t\tstyles.right = m.border.styleMultiTopRight\n\t}\n\n\tstyles.inherit(m.headerStyle)\n\n\treturn styles\n}\n\nfunc (m Model) styleRows() (inner borderStyleRow, last borderStyleRow) {\n\tif len(m.columns) == 1 {\n\t\tinner.left = m.border.styleSingleColumnInner\n\t\tinner.inner = inner.left\n\t\tinner.right = inner.left\n\n\t\tlast.left = m.border.styleSingleColumnBottom\n\n\t\tif m.hasFooter() {\n\t\t\tlast.left = m.border.styleBothWithFooter(last.left)\n\t\t}\n\n\t\tlast.inner = last.left\n\t\tlast.right = last.left\n\t} else {\n\t\tinner.left = m.border.styleMultiLeft\n\t\tinner.inner = m.border.styleMultiInner\n\t\tinner.right = m.border.styleMultiRight\n\n\t\tlast.left = m.border.styleMultiBottomLeft\n\t\tlast.inner = m.border.styleMultiBottom\n\t\tlast.right = m.border.styleMultiBottomRight\n\n\t\tif m.hasFooter() {\n\t\t\tlast.left = m.border.styleLeftWithFooter(last.left)\n\t\t\tlast.right = m.border.styleRightWithFooter(last.right)\n\t\t}\n\t}\n\n\treturn inner, last\n}\n"
  },
  {
    "path": "table/calc.go",
    "content": "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\tif x < y {\n\t\treturn x\n\t}\n\n\treturn y\n}\n\n// Keep compatibility with Go 1.21 by re-declaring max.\n//\n//nolint:predeclared\nfunc max(x, y int) int {\n\tif x > y {\n\t\treturn x\n\t}\n\n\treturn y\n}\n\n// These var names are fine for this little function\n//\n//nolint:varnamelen\nfunc gcd(x, y int) int {\n\tif x == 0 {\n\t\treturn y\n\t} else if y == 0 {\n\t\treturn x\n\t}\n\n\treturn gcd(y%x, x)\n}\n"
  },
  {
    "path": "table/calc_test.go",
    "content": "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 thorough!\n\nfunc TestMin(t *testing.T) {\n\ttests := []struct {\n\t\tx        int\n\t\ty        int\n\t\texpected int\n\t}{\n\t\t{\n\t\t\tx:        3,\n\t\t\ty:        4,\n\t\t\texpected: 3,\n\t\t},\n\t\t{\n\t\t\tx:        3,\n\t\t\ty:        3,\n\t\t\texpected: 3,\n\t\t},\n\t\t{\n\t\t\tx:        -4,\n\t\t\ty:        3,\n\t\t\texpected: -4,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(fmt.Sprintf(\"%d and %d gives %d\", test.x, test.y, test.expected), func(t *testing.T) {\n\t\t\tresult := min(test.x, test.y)\n\t\t\tassert.Equal(t, test.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestMax(t *testing.T) {\n\ttests := []struct {\n\t\tx        int\n\t\ty        int\n\t\texpected int\n\t}{\n\t\t{\n\t\t\tx:        3,\n\t\t\ty:        4,\n\t\t\texpected: 4,\n\t\t},\n\t\t{\n\t\t\tx:        3,\n\t\t\ty:        3,\n\t\t\texpected: 3,\n\t\t},\n\t\t{\n\t\t\tx:        -4,\n\t\t\ty:        3,\n\t\t\texpected: 3,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(fmt.Sprintf(\"%d and %d gives %d\", test.x, test.y, test.expected), func(t *testing.T) {\n\t\t\tresult := max(test.x, test.y)\n\t\t\tassert.Equal(t, test.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestGCD(t *testing.T) {\n\ttests := []struct {\n\t\tx        int\n\t\ty        int\n\t\texpected int\n\t}{\n\t\t{\n\t\t\tx:        3,\n\t\t\ty:        4,\n\t\t\texpected: 1,\n\t\t},\n\t\t{\n\t\t\tx:        3,\n\t\t\ty:        6,\n\t\t\texpected: 3,\n\t\t},\n\t\t{\n\t\t\tx:        4,\n\t\t\ty:        6,\n\t\t\texpected: 2,\n\t\t},\n\t\t{\n\t\t\tx:        0,\n\t\t\ty:        6,\n\t\t\texpected: 6,\n\t\t},\n\t\t{\n\t\t\tx:        12,\n\t\t\ty:        0,\n\t\t\texpected: 12,\n\t\t},\n\t\t{\n\t\t\tx:        1000,\n\t\t\ty:        100000,\n\t\t\texpected: 1000,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(fmt.Sprintf(\"%d and %d has GCD %d\", test.x, test.y, test.expected), func(t *testing.T) {\n\t\t\tresult := gcd(test.x, test.y)\n\t\t\tassert.Equal(t, test.expected, result)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "table/cell.go",
    "content": "package table\n\nimport \"github.com/charmbracelet/lipgloss\"\n\n// StyledCell represents a cell in the table that has a particular style applied.\n// The cell style takes highest precedence and will overwrite more general styles\n// from the row, column, or table as a whole.  This style should be generally\n// limited to colors, font style, and alignments - spacing style such as margin\n// will break the table format.\ntype StyledCell struct {\n\t// Data is the content of the cell.\n\tData any\n\n\t// Style is the specific style to apply. This is ignored if StyleFunc is not nil.\n\tStyle lipgloss.Style\n\n\t// StyleFunc is a function that takes the row/column of the cell and\n\t// returns a lipgloss.Style allowing for dynamic styling based on the cell's\n\t// content or position. Overrides Style if set.\n\tStyleFunc StyledCellFunc\n}\n\n// StyledCellFuncInput is the input to the StyledCellFunc. Sent as a struct\n// to allow for future additions without breaking changes.\ntype StyledCellFuncInput struct {\n\t// Data is the data in the cell.\n\tData any\n\n\t// Column is the column that the cell belongs to.\n\tColumn Column\n\n\t// Row is the row that the cell belongs to.\n\tRow Row\n\n\t// GlobalMetadata is the global table metadata that's been set by WithGlobalMetadata\n\tGlobalMetadata map[string]any\n}\n\n// StyledCellFunc is a function that takes various information about the cell and\n// returns a lipgloss.Style allowing for easier dynamic styling based on the cell's\n// content or position.\ntype StyledCellFunc = func(input StyledCellFuncInput) lipgloss.Style\n\n// NewStyledCell creates an entry that can be set in the row data and show as\n// styled with the given style.\nfunc NewStyledCell(data any, style lipgloss.Style) StyledCell {\n\treturn StyledCell{\n\t\tData:  data,\n\t\tStyle: style,\n\t}\n}\n\n// NewStyledCellWithStyleFunc creates an entry that can be set in the row data and show as\n// styled with the given style function.\nfunc NewStyledCellWithStyleFunc(data any, styleFunc StyledCellFunc) StyledCell {\n\treturn StyledCell{\n\t\tData:      data,\n\t\tStyleFunc: styleFunc,\n\t}\n}\n"
  },
  {
    "path": "table/column.go",
    "content": "package table\n\nimport (\n\t\"github.com/charmbracelet/lipgloss\"\n)\n\n// Column is a column in the table.\ntype Column struct {\n\ttitle string\n\tkey   string\n\twidth int\n\n\tflexFactor int\n\n\tfilterable bool\n\tstyle      lipgloss.Style\n\n\tfmtString string\n}\n\n// NewColumn creates a new fixed-width column with the given information.\nfunc NewColumn(key, title string, width int) Column {\n\treturn Column{\n\t\tkey:   key,\n\t\ttitle: title,\n\t\twidth: width,\n\n\t\tfilterable: false,\n\t}\n}\n\n// NewFlexColumn creates a new flexible width column that tries to fill in the\n// total table width.  If multiple flex columns exist, each will measure against\n// each other depending on their flexFactor.  For example, if both have a flexFactor\n// of 1, they will have equal width.  If one has a flexFactor of 1 and the other\n// has a flexFactor of 3, the second will be 3 times larger than the first.  You\n// must use WithTargetWidth if you have any flex columns, so that the table knows\n// how much width it should fill.\nfunc NewFlexColumn(key, title string, flexFactor int) Column {\n\treturn Column{\n\t\tkey:   key,\n\t\ttitle: title,\n\n\t\tflexFactor: max(flexFactor, 1),\n\t}\n}\n\n// WithStyle applies a style to the column as a whole.\nfunc (c Column) WithStyle(style lipgloss.Style) Column {\n\tc.style = style.Copy().Width(c.width)\n\n\treturn c\n}\n\n// WithFiltered sets whether the column should be considered for filtering (true)\n// or not (false).\nfunc (c Column) WithFiltered(filterable bool) Column {\n\tc.filterable = filterable\n\n\treturn c\n}\n\n// WithFormatString sets the format string used by fmt.Sprintf to display the data.\n// If not set, the default is \"%v\" for all data types.  Intended mainly for\n// numeric formatting.\n//\n// Since data is of the any type, make sure that all data in the column\n// is of the expected type or the format may fail.  For example, hardcoding '3'\n// instead of '3.0' and using '%.2f' will fail because '3' is an integer.\nfunc (c Column) WithFormatString(fmtString string) Column {\n\tc.fmtString = fmtString\n\n\treturn c\n}\n\nfunc (c *Column) isFlex() bool {\n\treturn c.flexFactor != 0\n}\n\n// Title returns the title of the column.\nfunc (c Column) Title() string {\n\treturn c.title\n}\n\n// Key returns the key of the column.\nfunc (c Column) Key() string {\n\treturn c.key\n}\n\n// Width returns the width of the column.\nfunc (c Column) Width() int {\n\treturn c.width\n}\n\n// FlexFactor returns the flex factor of the column.\nfunc (c Column) FlexFactor() int {\n\treturn c.flexFactor\n}\n\n// IsFlex returns whether the column is a flex column.\nfunc (c Column) IsFlex() bool {\n\treturn c.isFlex()\n}\n\n// Filterable returns whether the column is filterable.\nfunc (c Column) Filterable() bool {\n\treturn c.filterable\n}\n\n// Style returns the style of the column.\nfunc (c Column) Style() lipgloss.Style {\n\treturn c.style\n}\n\n// FmtString returns the format string of the column.\nfunc (c Column) FmtString() string {\n\treturn c.fmtString\n}\n"
  },
  {
    "path": "table/column_test.go",
    "content": "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\nfunc TestColumnTitle(t *testing.T) {\n\ttests := []struct {\n\t\ttitle    string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\ttitle:    \"foo\",\n\t\t\texpected: \"foo\",\n\t\t},\n\t\t{\n\t\t\ttitle:    \"bar\",\n\t\t\texpected: \"bar\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(fmt.Sprintf(\"title %s gives %s\", test.title, test.expected), func(t *testing.T) {\n\t\t\tcol := NewColumn(\"key\", test.title, 10)\n\t\t\tassert.Equal(t, test.expected, col.Title())\n\t\t})\n\t}\n}\n\nfunc TestColumnKey(t *testing.T) {\n\ttests := []struct {\n\t\tkey      string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tkey:      \"foo\",\n\t\t\texpected: \"foo\",\n\t\t},\n\t\t{\n\t\t\tkey:      \"bar\",\n\t\t\texpected: \"bar\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(fmt.Sprintf(\"key %s gives %s\", test.key, test.expected), func(t *testing.T) {\n\t\t\tcol := NewColumn(test.key, \"title\", 10)\n\t\t\tassert.Equal(t, test.expected, col.Key())\n\t\t})\n\t}\n}\n\nfunc TestColumnWidth(t *testing.T) {\n\ttests := []struct {\n\t\twidth    int\n\t\texpected int\n\t}{\n\t\t{\n\t\t\twidth:    3,\n\t\t\texpected: 3,\n\t\t},\n\t\t{\n\t\t\twidth:    4,\n\t\t\texpected: 4,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(fmt.Sprintf(\"width %d gives %d\", test.width, test.expected), func(t *testing.T) {\n\t\t\tcol := NewColumn(\"key\", \"title\", test.width)\n\t\t\tassert.Equal(t, test.expected, col.Width())\n\t\t})\n\t}\n}\n\nfunc TestColumnFlexFactor(t *testing.T) {\n\ttests := []struct {\n\t\tflexFactor int\n\t\texpected   int\n\t}{\n\t\t{\n\t\t\tflexFactor: 3,\n\t\t\texpected:   3,\n\t\t},\n\t\t{\n\t\t\tflexFactor: 4,\n\t\t\texpected:   4,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(fmt.Sprintf(\"flexFactor %d gives %d\", test.flexFactor, test.expected), func(t *testing.T) {\n\t\t\tcol := NewFlexColumn(\"key\", \"title\", test.flexFactor)\n\t\t\tassert.Equal(t, test.expected, col.FlexFactor())\n\t\t})\n\t}\n}\n\nfunc TestColumnIsFlex(t *testing.T) {\n\ttestsFlexColumn := []struct {\n\t\tflexFactor int\n\t\texpected   bool\n\t}{\n\t\t{\n\t\t\tflexFactor: 3,\n\t\t\texpected:   true,\n\t\t},\n\t\t{\n\t\t\tflexFactor: 0,\n\t\t\texpected:   true,\n\t\t},\n\t}\n\n\tfor _, test := range testsFlexColumn {\n\t\tt.Run(fmt.Sprintf(\"flexFactor %d gives %t\", test.flexFactor, test.expected), func(t *testing.T) {\n\t\t\tcol := NewFlexColumn(\"key\", \"title\", test.flexFactor)\n\t\t\tassert.Equal(t, test.expected, col.IsFlex())\n\t\t})\n\t}\n\n\ttestsRegularColumn := []struct {\n\t\twidth    int\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\twidth:    3,\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\twidth:    0,\n\t\t\texpected: false,\n\t\t},\n\t}\n\n\tfor _, test := range testsRegularColumn {\n\t\tt.Run(fmt.Sprintf(\"width %d gives %t\", test.width, test.expected), func(t *testing.T) {\n\t\t\tcol := NewColumn(\"key\", \"title\", test.width)\n\t\t\tassert.Equal(t, test.expected, col.IsFlex())\n\t\t})\n\t}\n}\n\nfunc TestColumnFilterable(t *testing.T) {\n\ttests := []struct {\n\t\tfilterable bool\n\t\texpected   bool\n\t}{\n\t\t{\n\t\t\tfilterable: true,\n\t\t\texpected:   true,\n\t\t},\n\t\t{\n\t\t\tfilterable: false,\n\t\t\texpected:   false,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(fmt.Sprintf(\"filterable %t gives %t\", test.filterable, test.expected), func(t *testing.T) {\n\t\t\tcol := NewColumn(\"key\", \"title\", 10)\n\t\t\tcol = col.WithFiltered(test.filterable)\n\t\t\tassert.Equal(t, test.expected, col.Filterable())\n\t\t})\n\t}\n}\n\nfunc TestColumnStyle(t *testing.T) {\n\twidth := 10\n\ttests := []struct {\n\t\tstyle    lipgloss.Style\n\t\texpected lipgloss.Style\n\t}{\n\t\t{\n\t\t\tstyle:    lipgloss.NewStyle(),\n\t\t\texpected: lipgloss.NewStyle().Width(width),\n\t\t},\n\t\t{\n\t\t\tstyle:    lipgloss.NewStyle().Bold(true),\n\t\t\texpected: lipgloss.NewStyle().Bold(true).Width(width),\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(fmt.Sprintf(\"style %v gives %v\", test.style, test.expected), func(t *testing.T) {\n\t\t\tcol := NewColumn(\"key\", \"title\", width).WithStyle(test.style)\n\t\t\tassert.Equal(t, test.expected, col.Style())\n\t\t})\n\t}\n}\n\nfunc TestColumnFormatString(t *testing.T) {\n\ttests := []struct {\n\t\tfmtString string\n\t\texpected  string\n\t}{\n\t\t{\n\t\t\tfmtString: \"%v\",\n\t\t\texpected:  \"%v\",\n\t\t},\n\t\t{\n\t\t\tfmtString: \"%.2f\",\n\t\t\texpected:  \"%.2f\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(fmt.Sprintf(\"fmtString %s gives %s\", test.fmtString, test.expected), func(t *testing.T) {\n\t\t\tcol := NewColumn(\"key\", \"title\", 10)\n\t\t\tcol = col.WithFormatString(test.fmtString)\n\t\t\tassert.Equal(t, test.expected, col.FmtString())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "table/data.go",
    "content": "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 asInt(data any) (int64, bool) {\n\tswitch val := data.(type) {\n\tcase int:\n\t\treturn int64(val), true\n\n\tcase int8:\n\t\treturn int64(val), true\n\n\tcase int16:\n\t\treturn int64(val), true\n\n\tcase int32:\n\t\treturn int64(val), true\n\n\tcase int64:\n\t\treturn val, true\n\n\tcase uint:\n\t\t// #nosec: G115\n\t\treturn int64(val), true\n\n\tcase uint8:\n\t\treturn int64(val), true\n\n\tcase uint16:\n\t\treturn int64(val), true\n\n\tcase uint32:\n\t\treturn int64(val), true\n\n\tcase uint64:\n\t\t// #nosec: G115\n\t\treturn int64(val), true\n\n\tcase time.Duration:\n\t\treturn int64(val), true\n\n\tcase StyledCell:\n\t\treturn asInt(val.Data)\n\t}\n\n\treturn 0, false\n}\n\nfunc asNumber(data any) (float64, bool) {\n\tswitch val := data.(type) {\n\tcase float32:\n\t\treturn float64(val), true\n\n\tcase float64:\n\t\treturn val, true\n\n\tcase StyledCell:\n\t\treturn asNumber(val.Data)\n\t}\n\n\tintVal, isInt := asInt(data)\n\n\treturn float64(intVal), isInt\n}\n"
  },
  {
    "path": "table/data_test.go",
    "content": "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\tcheck := func(data any, isInt bool, expectedValue int64) {\n\t\tval, ok := asInt(data)\n\t\tassert.Equal(t, isInt, ok)\n\t\tassert.Equal(t, expectedValue, val)\n\t}\n\n\tcheck(3, true, 3)\n\tcheck(3.3, false, 0)\n\tcheck(int8(3), true, 3)\n\tcheck(int16(3), true, 3)\n\tcheck(int32(3), true, 3)\n\tcheck(int64(3), true, 3)\n\tcheck(uint(3), true, 3)\n\tcheck(uint8(3), true, 3)\n\tcheck(uint16(3), true, 3)\n\tcheck(uint32(3), true, 3)\n\tcheck(uint64(3), true, 3)\n\tcheck(StyledCell{Data: 3}, true, 3)\n\tcheck(time.Duration(3), true, 3)\n}\n\nfunc TestAsNumber(t *testing.T) {\n\tcheck := func(data any, isFloat bool, expectedValue float64) {\n\t\tval, ok := asNumber(data)\n\t\tassert.Equal(t, isFloat, ok)\n\t\tassert.InDelta(t, expectedValue, val, 0.001)\n\t}\n\n\tcheck(uint32(3), true, 3)\n\tcheck(3.3, true, 3.3)\n\tcheck(float32(3.3), true, 3.3)\n\tcheck(StyledCell{Data: 3.3}, true, 3.3)\n}\n"
  },
  {
    "path": "table/dimensions.go",
    "content": "package table\n\nimport (\n\t\"github.com/charmbracelet/lipgloss\"\n)\n\nfunc (m *Model) recalculateWidth() {\n\tif m.targetTotalWidth != 0 {\n\t\tm.totalWidth = m.targetTotalWidth\n\t} else {\n\t\ttotal := 0\n\n\t\tfor _, column := range m.columns {\n\t\t\ttotal += column.width\n\t\t}\n\n\t\tm.totalWidth = total + len(m.columns) + 1\n\t}\n\n\tupdateColumnWidths(m.columns, m.targetTotalWidth)\n\n\tm.recalculateLastHorizontalColumn()\n}\n\n// Updates column width in-place.  This could be optimized but should be called\n// very rarely so we prioritize simplicity over performance here.\nfunc updateColumnWidths(cols []Column, totalWidth int) {\n\ttotalFlexWidth := totalWidth - len(cols) - 1\n\ttotalFlexFactor := 0\n\tflexGCD := 0\n\n\tfor index, col := range cols {\n\t\tif !col.isFlex() {\n\t\t\ttotalFlexWidth -= col.width\n\t\t\tcols[index].style = col.style.Width(col.width)\n\t\t} else {\n\t\t\ttotalFlexFactor += col.flexFactor\n\t\t\tflexGCD = gcd(flexGCD, col.flexFactor)\n\t\t}\n\t}\n\n\tif totalFlexFactor == 0 {\n\t\treturn\n\t}\n\n\t// We use the GCD here because otherwise very large values won't divide\n\t// nicely as ints\n\ttotalFlexFactor /= flexGCD\n\n\tflexUnit := totalFlexWidth / totalFlexFactor\n\tleftoverWidth := totalFlexWidth % totalFlexFactor\n\n\tfor index := range cols {\n\t\tif !cols[index].isFlex() {\n\t\t\tcontinue\n\t\t}\n\n\t\twidth := flexUnit * (cols[index].flexFactor / flexGCD)\n\n\t\tif leftoverWidth > 0 {\n\t\t\twidth++\n\t\t\tleftoverWidth--\n\t\t}\n\n\t\tif index == len(cols)-1 {\n\t\t\twidth += leftoverWidth\n\t\t\tleftoverWidth = 0\n\t\t}\n\n\t\twidth = max(width, 1)\n\n\t\tcols[index].width = width\n\n\t\t// Take borders into account for the actual style\n\t\tcols[index].style = cols[index].style.Width(width)\n\t}\n}\n\nfunc (m *Model) recalculateHeight() {\n\theader := m.renderHeaders()\n\theaderHeight := 1 // Header always has the top border\n\tif m.headerVisible {\n\t\theaderHeight = lipgloss.Height(header)\n\t}\n\n\tfooter := m.renderFooter(lipgloss.Width(header), false)\n\tvar footerHeight int\n\tif footer != \"\" {\n\t\tfooterHeight = lipgloss.Height(footer)\n\t}\n\n\tm.metaHeight = headerHeight + footerHeight\n}\n\nfunc (m *Model) calculatePadding(numRows int) int {\n\tif m.minimumHeight == 0 {\n\t\treturn 0\n\t}\n\n\tpadding := m.minimumHeight - m.metaHeight - numRows - 1 // additional 1 for bottom border\n\n\tif padding == 0 && numRows == 0 {\n\t\t// This is an edge case where we want to add 1 additional line of height, i.e.\n\t\t// add a border without an empty row. However, this is not possible, so we need\n\t\t// to add an extra row which will result in the table being 1 row taller than\n\t\t// the requested minimum height.\n\t\treturn 1\n\t}\n\n\tif padding < 0 {\n\t\t// Table is already larger than minimum height, do nothing.\n\t\treturn 0\n\t}\n\n\treturn padding\n}\n"
  },
  {
    "path": "table/dimensions_test.go",
    "content": "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 because of repetitive test definitions, this is fine\n//\n//nolint:funlen\nfunc TestColumnUpdateWidths(t *testing.T) {\n\ttests := []struct {\n\t\tname           string\n\t\tcolumns        []Column\n\t\ttotalWidth     int\n\t\texpectedWidths []int\n\t}{\n\t\t{\n\t\t\tname: \"Static\",\n\t\t\tcolumns: []Column{\n\t\t\t\tNewColumn(\"abc\", \"a\", 4),\n\t\t\t\tNewColumn(\"sdf\", \"b\", 7),\n\t\t\t\tNewColumn(\"xyz\", \"c\", 2),\n\t\t\t},\n\t\t\ttotalWidth: 13,\n\t\t\texpectedWidths: []int{\n\t\t\t\t4, 7, 2,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Even half\",\n\t\t\tcolumns: []Column{\n\t\t\t\tNewFlexColumn(\"abc\", \"a\", 1),\n\t\t\t\tNewFlexColumn(\"sdf\", \"b\", 1),\n\t\t\t},\n\t\t\ttotalWidth: 11,\n\t\t\texpectedWidths: []int{\n\t\t\t\t4, 4,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Odd half increases first\",\n\t\t\tcolumns: []Column{\n\t\t\t\tNewFlexColumn(\"abc\", \"a\", 1),\n\t\t\t\tNewFlexColumn(\"sdf\", \"b\", 1),\n\t\t\t},\n\t\t\ttotalWidth: 12,\n\t\t\texpectedWidths: []int{\n\t\t\t\t5, 4,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Even fourths\",\n\t\t\tcolumns: []Column{\n\t\t\t\tNewFlexColumn(\"abc\", \"a\", 1),\n\t\t\t\tNewFlexColumn(\"sdf\", \"b\", 1),\n\t\t\t\tNewFlexColumn(\"xyz\", \"c\", 1),\n\t\t\t\tNewFlexColumn(\"123\", \"d\", 1),\n\t\t\t},\n\t\t\ttotalWidth: 17,\n\t\t\texpectedWidths: []int{\n\t\t\t\t3, 3, 3, 3,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Odd fourths\",\n\t\t\tcolumns: []Column{\n\t\t\t\tNewFlexColumn(\"abc\", \"a\", 1),\n\t\t\t\tNewFlexColumn(\"sdf\", \"b\", 1),\n\t\t\t\tNewFlexColumn(\"xyz\", \"c\", 1),\n\t\t\t\tNewFlexColumn(\"123\", \"d\", 1),\n\t\t\t},\n\t\t\ttotalWidth: 20,\n\t\t\texpectedWidths: []int{\n\t\t\t\t4, 4, 4, 3,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Simple mix static and flex\",\n\t\t\tcolumns: []Column{\n\t\t\t\tNewColumn(\"abc\", \"a\", 5),\n\t\t\t\tNewFlexColumn(\"flex\", \"flex\", 1),\n\t\t\t},\n\t\t\ttotalWidth: 18,\n\t\t\texpectedWidths: []int{\n\t\t\t\t5, 10,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Static and flex with high flex factor\",\n\t\t\tcolumns: []Column{\n\t\t\t\tNewColumn(\"abc\", \"a\", 5),\n\t\t\t\tNewFlexColumn(\"flex\", \"flex\", 1000),\n\t\t\t},\n\t\t\ttotalWidth: 18,\n\t\t\texpectedWidths: []int{\n\t\t\t\t5, 10,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Static and multiple flexes with high flex factor\",\n\t\t\tcolumns: []Column{\n\t\t\t\tNewColumn(\"abc\", \"a\", 5),\n\t\t\t\tNewFlexColumn(\"flex\", \"flex\", 1000),\n\t\t\t\tNewFlexColumn(\"flex\", \"flex\", 1000),\n\t\t\t\tNewFlexColumn(\"flex\", \"flex\", 1000),\n\t\t\t},\n\t\t\ttotalWidth: 22,\n\t\t\texpectedWidths: []int{\n\t\t\t\t5, 4, 4, 4,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Static and multiple flexes of different sizes\",\n\t\t\tcolumns: []Column{\n\t\t\t\tNewFlexColumn(\"flex\", \"flex\", 1),\n\t\t\t\tNewColumn(\"abc\", \"a\", 5),\n\t\t\t\tNewFlexColumn(\"flex\", \"flex\", 2),\n\t\t\t\tNewFlexColumn(\"flex\", \"flex\", 1),\n\t\t\t},\n\t\t\ttotalWidth: 22,\n\t\t\texpectedWidths: []int{\n\t\t\t\t3, 5, 6, 3,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Width is too small\",\n\t\t\tcolumns: []Column{\n\t\t\t\tNewColumn(\"abc\", \"a\", 5),\n\t\t\t\tNewFlexColumn(\"flex\", \"flex\", 2),\n\t\t\t\tNewFlexColumn(\"flex\", \"flex\", 1),\n\t\t\t},\n\t\t\ttotalWidth: 3,\n\t\t\texpectedWidths: []int{\n\t\t\t\t5, 1, 1,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tupdateColumnWidths(test.columns, test.totalWidth)\n\n\t\t\tfor i, col := range test.columns {\n\t\t\t\tassert.Equal(t, test.expectedWidths[i], col.width, fmt.Sprintf(\"index %d\", i))\n\t\t\t}\n\t\t})\n\t}\n}\n\n// This function is long because it has many test cases\n//\n//nolint:funlen\nfunc TestRecalculateHeight(t *testing.T) {\n\tcolumns := []Column{\n\t\tNewColumn(\"ka\", \"a\", 3),\n\t\tNewColumn(\"kb\", \"b\", 4),\n\t\tNewColumn(\"kc\", \"c\", 5),\n\t}\n\n\trows := []Row{\n\t\tNewRow(RowData{\"ka\": 1, \"kb\": 23, \"kc\": \"zyx\"}),\n\t\tNewRow(RowData{\"ka\": 3, \"kb\": 34, \"kc\": \"wvu\"}),\n\t\tNewRow(RowData{\"ka\": 5, \"kb\": 45, \"kc\": \"zyx\"}),\n\t\tNewRow(RowData{\"ka\": 7, \"kb\": 56, \"kc\": \"wvu\"}),\n\t}\n\n\ttests := []struct {\n\t\tname           string\n\t\tmodel          Model\n\t\texpectedHeight int\n\t}{\n\t\t{\n\t\t\tname:           \"Default header\",\n\t\t\tmodel:          New(columns).WithRows(rows),\n\t\t\texpectedHeight: 3,\n\t\t},\n\t\t{\n\t\t\tname:           \"Empty page with default header\",\n\t\t\tmodel:          New(columns),\n\t\t\texpectedHeight: 3,\n\t\t},\n\t\t{\n\t\t\tname:           \"Filtered with default header\",\n\t\t\tmodel:          New(columns).WithRows(rows).Filtered(true),\n\t\t\texpectedHeight: 5,\n\t\t},\n\t\t{\n\t\t\tname:           \"Static footer one line\",\n\t\t\tmodel:          New(columns).WithRows(rows).WithStaticFooter(\"single line\"),\n\t\t\texpectedHeight: 5,\n\t\t},\n\t\t{\n\t\t\tname: \"Static footer overflow\",\n\t\t\tmodel: New(columns).WithRows(rows).\n\t\t\t\tWithStaticFooter(\"single line but it's long\"),\n\t\t\texpectedHeight: 6,\n\t\t},\n\t\t{\n\t\t\tname: \"Static footer multi-line\",\n\t\t\tmodel: New(columns).WithRows(rows).\n\t\t\t\tWithStaticFooter(\"footer with\\nmultiple lines\"),\n\t\t\texpectedHeight: 6,\n\t\t},\n\t\t{\n\t\t\tname:           \"Paginated\",\n\t\t\tmodel:          New(columns).WithRows(rows).WithPageSize(2),\n\t\t\texpectedHeight: 5,\n\t\t},\n\t\t{\n\t\t\tname:           \"No pagination\",\n\t\t\tmodel:          New(columns).WithRows(rows).WithPageSize(2).WithNoPagination(),\n\t\t\texpectedHeight: 3,\n\t\t},\n\t\t{\n\t\t\tname:           \"Footer not visible\",\n\t\t\tmodel:          New(columns).WithRows(rows).Filtered(true).WithFooterVisibility(false),\n\t\t\texpectedHeight: 3,\n\t\t},\n\t\t{\n\t\t\tname:           \"Header not visible\",\n\t\t\tmodel:          New(columns).WithRows(rows).WithHeaderVisibility(false),\n\t\t\texpectedHeight: 1,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\ttest.model.recalculateHeight()\n\t\t\tassert.Equal(t, test.expectedHeight, test.model.metaHeight)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "table/doc.go",
    "content": "/*\nPackage table contains a Bubble Tea component for an interactive and customizable\ntable.\n\nThe simplest useful table can be created with table.New(...).WithRows(...).  Row\ndata should map to the column keys, as shown below.  Note that extra data will\nsimply not be shown, while missing data will be safely blank in the row's cell.\n\n\tconst (\n\t\t// This is not necessary, but recommended to avoid typos\n\t\tcolumnKeyName  = \"name\"\n\t\tcolumnKeyCount = \"count\"\n\t)\n\n\t// Define the columns and how they appear\n\tcolumns := []table.Column{\n\t\ttable.NewColumn(columnKeyName, \"Name\", 10),\n\t\ttable.NewColumn(columnKeyCount, \"Count\", 6),\n\t}\n\n\t// Define the data that will be in the table, mapping to the column keys\n\trows := []table.Row{\n\t\ttable.NewRow(table.RowData{\n\t\t\tcolumnKeyName:  \"Cheeseburger\",\n\t\t\tcolumnKeyCount: 3,\n\t\t}),\n\t\ttable.NewRow(table.RowData{\n\t\t\tcolumnKeyName:  \"Fries\",\n\t\t\tcolumnKeyCount: 2,\n\t\t}),\n\t}\n\n\t// Create the table\n\ttbl := table.New(columns).WithRows(rows)\n\n\t// Use it like any Bubble Tea component in your view\n\ttbl.View()\n*/\npackage table\n"
  },
  {
    "path": "table/events.go",
    "content": "package table\n\n// UserEvent is some state change that has occurred due to user input.  These will\n// ONLY be generated when a user has interacted directly with the table.  These\n// will NOT be generated when code programmatically changes values in the table.\ntype UserEvent any\n\nfunc (m *Model) appendUserEvent(e UserEvent) {\n\tm.lastUpdateUserEvents = append(m.lastUpdateUserEvents, e)\n}\n\nfunc (m *Model) clearUserEvents() {\n\tm.lastUpdateUserEvents = nil\n}\n\n// GetLastUpdateUserEvents returns a list of events that happened due to user\n// input in the last Update call.  This is useful to look for triggers such as\n// whether the user moved to a new highlighted row.\nfunc (m *Model) GetLastUpdateUserEvents() []UserEvent {\n\t// Most common case\n\tif len(m.lastUpdateUserEvents) == 0 {\n\t\treturn nil\n\t}\n\n\treturned := make([]UserEvent, len(m.lastUpdateUserEvents))\n\n\t// Slightly wasteful but helps guarantee immutability, and this should only\n\t// have data very rarely so this is fine\n\tcopy(returned, m.lastUpdateUserEvents)\n\n\treturn returned\n}\n\n// UserEventHighlightedIndexChanged indicates that the user has scrolled to a new\n// row.\ntype UserEventHighlightedIndexChanged struct {\n\t// PreviousRow is the row that was selected before the change.\n\tPreviousRowIndex int\n\n\t// SelectedRow is the row index that is now selected\n\tSelectedRowIndex int\n}\n\n// UserEventRowSelectToggled indicates that the user has either selected or\n// deselected a row by toggling the selection.  The event contains information\n// about which row index was selected and whether it was selected or deselected.\ntype UserEventRowSelectToggled struct {\n\tRowIndex   int\n\tIsSelected bool\n}\n\n// UserEventFilterInputFocused indicates that the user has focused the filter\n// text input, so that any other typing will type into the filter field.  Only\n// activates for the built-in filter text box.\ntype UserEventFilterInputFocused struct{}\n\n// UserEventFilterInputUnfocused indicates that the user has unfocused the filter\n// text input, which means the user is done typing into the filter field.  Only\n// activates for the built-in filter text box.\ntype UserEventFilterInputUnfocused struct{}\n"
  },
  {
    "path": "table/events_test.go",
    "content": "package table\n\nimport (\n\t\"testing\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestUserEventsEmptyWhenNothingHappens(t *testing.T) {\n\tmodel := New([]Column{})\n\n\tevents := model.GetLastUpdateUserEvents()\n\n\tassert.Len(t, events, 0, \"Should be empty when nothing has happened\")\n\n\tmodel, _ = model.Update(nil)\n\n\tevents = model.GetLastUpdateUserEvents()\n\n\tassert.Len(t, events, 0, \"Should be empty when no changes made in Update\")\n}\n\n//nolint:funlen // This is a bunch of checks in a row, this is fine\nfunc TestUserEventHighlightedIndexChanged(t *testing.T) {\n\t// Don't need any actual row data for this\n\tempty := RowData{}\n\n\tmodel := New([]Column{}).\n\t\tFocused(true).\n\t\tWithRows(\n\t\t\t[]Row{\n\t\t\t\tNewRow(empty),\n\t\t\t\tNewRow(empty),\n\t\t\t\tNewRow(empty),\n\t\t\t\tNewRow(empty),\n\t\t\t},\n\t\t)\n\n\thitDown := func() {\n\t\tmodel, _ = model.Update(tea.KeyMsg{Type: tea.KeyDown})\n\t}\n\n\thitUp := func() {\n\t\tmodel, _ = model.Update(tea.KeyMsg{Type: tea.KeyUp})\n\t}\n\n\tcheckEvent := func(events []UserEvent, expectedPreviousIndex, expectedCurrentIndex int) {\n\t\tif len(events) != 1 {\n\t\t\tassert.FailNow(t, \"Asked to check events with len of not 1, test is bad\")\n\t\t}\n\n\t\tswitch event := events[0].(type) {\n\t\tcase UserEventHighlightedIndexChanged:\n\t\t\tassert.Equal(t, expectedPreviousIndex, event.PreviousRowIndex)\n\t\t\tassert.Equal(t, expectedCurrentIndex, event.SelectedRowIndex)\n\n\t\tdefault:\n\t\t\tassert.Failf(t, \"Event is not expected type UserEventHighlightedIndexChanged\", \"%+v\", event)\n\t\t}\n\t}\n\n\tevents := model.GetLastUpdateUserEvents()\n\tassert.Len(t, events, 0, \"Should be empty when nothing has happened\")\n\n\t// Hit down to change row down by one\n\thitDown()\n\tevents = model.GetLastUpdateUserEvents()\n\tassert.Len(t, events, 1, \"Missing event for scrolling down\")\n\tcheckEvent(events, 0, 1)\n\n\t// Do some no-op\n\tmodel, _ = model.Update(nil)\n\tevents = model.GetLastUpdateUserEvents()\n\tassert.Len(t, events, 0, \"Events not cleared between Updates\")\n\n\t// Hit up to go back to top\n\thitUp()\n\tevents = model.GetLastUpdateUserEvents()\n\tassert.Len(t, events, 1, \"Missing event to scroll back up\")\n\tcheckEvent(events, 1, 0)\n\n\t// Hit up to scroll around to bottom\n\thitUp()\n\tevents = model.GetLastUpdateUserEvents()\n\tassert.Len(t, events, 1, \"Missing event to scroll up with wrap\")\n\tcheckEvent(events, 0, 3)\n\n\t// Now check to make sure it doesn't trigger when row index doesn't change\n\tmodel = model.WithRows([]Row{NewRow(empty)})\n\thitDown()\n\tevents = model.GetLastUpdateUserEvents()\n\tassert.Len(t, events, 0, \"There's no row to change to for single row table, event shouldn't exist\")\n\n\tmodel = model.WithRows([]Row{})\n\thitDown()\n\tevents = model.GetLastUpdateUserEvents()\n\tassert.Len(t, events, 0, \"There's no row to change to for an empty table, event shouldn't exist\")\n}\n\n//nolint:funlen // This is a bunch of checks in a row, this is fine\nfunc TestUserEventRowSelectToggled(t *testing.T) {\n\t// Don't need any actual row data for this\n\tempty := RowData{}\n\n\tmodel := New([]Column{}).\n\t\tFocused(true).\n\t\tWithRows(\n\t\t\t[]Row{\n\t\t\t\tNewRow(empty),\n\t\t\t\tNewRow(empty),\n\t\t\t\tNewRow(empty),\n\t\t\t\tNewRow(empty),\n\t\t\t},\n\t\t).\n\t\tSelectableRows(true)\n\n\thitDown := func() {\n\t\tmodel, _ = model.Update(tea.KeyMsg{Type: tea.KeyDown})\n\t}\n\n\thitSelectToggle := func() {\n\t\tmodel, _ = model.Update(tea.KeyMsg{Type: tea.KeySpace})\n\t}\n\n\tcheckEvent := func(events []UserEvent, expectedRowIndex int, expectedSelectionState bool) {\n\t\tif len(events) != 1 {\n\t\t\tassert.FailNow(t, \"Asked to check events with len of not 1, test is bad\")\n\t\t}\n\n\t\tswitch event := events[0].(type) {\n\t\tcase UserEventRowSelectToggled:\n\t\t\tassert.Equal(t, expectedRowIndex, event.RowIndex, \"Row index wrong\")\n\t\t\tassert.Equal(t, expectedSelectionState, event.IsSelected, \"Selection state wrong\")\n\n\t\tdefault:\n\t\t\tassert.Failf(t, \"Event is not expected type UserEventRowSelectToggled\", \"%+v\", event)\n\t\t}\n\t}\n\n\tevents := model.GetLastUpdateUserEvents()\n\tassert.Len(t, events, 0, \"Should be empty when nothing has happened\")\n\n\t// Try initial selection\n\thitSelectToggle()\n\tevents = model.GetLastUpdateUserEvents()\n\tassert.Len(t, events, 1, \"Missing event for selection toggle\")\n\tcheckEvent(events, 0, true)\n\n\t// Do some no-op\n\tmodel, _ = model.Update(nil)\n\tevents = model.GetLastUpdateUserEvents()\n\tassert.Len(t, events, 0, \"Events not cleared between Updates\")\n\n\t// Check deselection\n\thitSelectToggle()\n\tevents = model.GetLastUpdateUserEvents()\n\tassert.Len(t, events, 1, \"Missing event to toggle select for second time\")\n\tcheckEvent(events, 0, false)\n\n\t// Try one row down... note that the row change event should clear after the\n\t// first keypress\n\thitDown()\n\thitSelectToggle()\n\tevents = model.GetLastUpdateUserEvents()\n\tassert.Len(t, events, 1, \"Missing event after scrolling down\")\n\tcheckEvent(events, 1, true)\n\n\t// Check edge case of empty table\n\tmodel = model.WithRows([]Row{})\n\thitSelectToggle()\n\tevents = model.GetLastUpdateUserEvents()\n\tassert.Len(t, events, 0, \"There's no row to select for an empty table, event shouldn't exist\")\n}\n\nfunc TestFilterFocusEvents(t *testing.T) {\n\tmodel := New([]Column{}).Filtered(true).Focused(true)\n\n\tevents := model.GetLastUpdateUserEvents()\n\n\tassert.Empty(t, events, \"Unexpected events to start\")\n\n\t// Start filter\n\tmodel, _ = model.Update(tea.KeyMsg{\n\t\tType:  tea.KeyRunes,\n\t\tRunes: []rune{'/'},\n\t})\n\tevents = model.GetLastUpdateUserEvents()\n\tassert.Len(t, events, 1, \"Only expected one event\")\n\tswitch events[0].(type) {\n\tcase UserEventFilterInputFocused:\n\tdefault:\n\t\tassert.FailNow(t, \"Unexpected event type\")\n\t}\n\n\t// Stop filter\n\tmodel, _ = model.Update(tea.KeyMsg{\n\t\tType: tea.KeyEnter,\n\t})\n\tevents = model.GetLastUpdateUserEvents()\n\tassert.Len(t, events, 1, \"Only expected one event\")\n\tswitch events[0].(type) {\n\tcase UserEventFilterInputUnfocused:\n\tdefault:\n\t\tassert.FailNow(t, \"Unexpected event type\")\n\t}\n}\n"
  },
  {
    "path": "table/filter.go",
    "content": "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 add more things later\n// without breaking compatibility.\ntype FilterFuncInput struct {\n\t// Columns is a list of the columns of the table\n\tColumns []Column\n\n\t// Row is the row that's being considered for filtering\n\tRow Row\n\n\t// GlobalMetadata is an arbitrary set of metadata from the table set by WithGlobalMetadata\n\tGlobalMetadata map[string]any\n\n\t// Filter is the filter string input to consider\n\tFilter string\n}\n\n// FilterFunc takes a FilterFuncInput and returns true if the row should be visible,\n// or false if the row should be hidden.\ntype FilterFunc func(FilterFuncInput) bool\n\nfunc (m Model) getFilteredRows(rows []Row) []Row {\n\tfilterInputValue := m.filterTextInput.Value()\n\tif !m.filtered || filterInputValue == \"\" {\n\t\treturn rows\n\t}\n\n\tfilteredRows := make([]Row, 0)\n\n\tfor _, row := range rows {\n\t\tvar availableFilterFunc FilterFunc\n\n\t\tif m.filterFunc != nil {\n\t\t\tavailableFilterFunc = m.filterFunc\n\t\t} else {\n\t\t\tavailableFilterFunc = filterFuncContains\n\t\t}\n\n\t\tif availableFilterFunc(FilterFuncInput{\n\t\t\tColumns:        m.columns,\n\t\t\tRow:            row,\n\t\t\tFilter:         filterInputValue,\n\t\t\tGlobalMetadata: m.metadata,\n\t\t}) {\n\t\t\tfilteredRows = append(filteredRows, row)\n\t\t}\n\t}\n\n\treturn filteredRows\n}\n\n// filterFuncContains returns a filterFunc that performs case-insensitive\n// \"contains\" matching over all filterable columns in a row.\nfunc filterFuncContains(input FilterFuncInput) bool {\n\tif input.Filter == \"\" {\n\t\treturn true\n\t}\n\n\tcheckedAny := false\n\n\tfilterLower := strings.ToLower(input.Filter)\n\n\tfor _, column := range input.Columns {\n\t\tif !column.filterable {\n\t\t\tcontinue\n\t\t}\n\n\t\tcheckedAny = true\n\n\t\tdata, ok := input.Row.Data[column.key]\n\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Extract internal StyledCell data\n\t\tswitch dataV := data.(type) {\n\t\tcase StyledCell:\n\t\t\tdata = dataV.Data\n\t\t}\n\n\t\tvar target string\n\t\tswitch dataV := data.(type) {\n\t\tcase string:\n\t\t\ttarget = dataV\n\n\t\tcase fmt.Stringer:\n\t\t\ttarget = dataV.String()\n\n\t\tdefault:\n\t\t\ttarget = fmt.Sprintf(\"%v\", data)\n\t\t}\n\n\t\tif strings.Contains(strings.ToLower(target), filterLower) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn !checkedAny\n}\n\n// filterFuncFuzzy returns a filterFunc that performs case-insensitive fuzzy\n// matching (subsequence) over the concatenation of all filterable column values.\nfunc filterFuncFuzzy(input FilterFuncInput) bool {\n\tfilter := strings.TrimSpace(input.Filter)\n\tif filter == \"\" {\n\t\treturn true\n\t}\n\n\tvar builder strings.Builder\n\tfor _, col := range input.Columns {\n\t\tif !col.filterable {\n\t\t\tcontinue\n\t\t}\n\t\tvalue, ok := input.Row.Data[col.key]\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\tif sc, ok := value.(StyledCell); ok {\n\t\t\tvalue = sc.Data\n\t\t}\n\t\tbuilder.WriteString(fmt.Sprint(value)) // uses Stringer if implemented\n\t\tbuilder.WriteByte(' ')\n\t}\n\n\thaystack := strings.ToLower(builder.String())\n\tif haystack == \"\" {\n\t\treturn false\n\t}\n\n\tfor _, token := range strings.Fields(strings.ToLower(filter)) {\n\t\tif !fuzzySubsequenceMatch(haystack, token) {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n\n// fuzzySubsequenceMatch returns true if all runes in needle appear in order\n// within haystack (not necessarily contiguously). Case must be normalized by caller.\nfunc fuzzySubsequenceMatch(haystack, needle string) bool {\n\tif needle == \"\" {\n\t\treturn true\n\t}\n\thaystackIndex, needleIndex := 0, 0\n\thaystackRunes := []rune(haystack)\n\tneedleRunes := []rune(needle)\n\n\tfor haystackIndex < len(haystackRunes) && needleIndex < len(needleRunes) {\n\t\tif haystackRunes[haystackIndex] == needleRunes[needleIndex] {\n\t\t\tneedleIndex++\n\t\t}\n\t\thaystackIndex++\n\t}\n\n\treturn needleIndex == len(needleRunes)\n}\n"
  },
  {
    "path": "table/filter_test.go",
    "content": "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/charmbracelet/bubbletea\"\n\t\"github.com/charmbracelet/lipgloss\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestIsRowMatched(t *testing.T) {\n\tcolumns := []Column{\n\t\tNewColumn(\"title\", \"title\", 10).WithFiltered(true),\n\t\tNewColumn(\"description\", \"description\", 10)}\n\n\tassert.True(t, filterFuncContains(FilterFuncInput{\n\t\tColumns: columns,\n\t\tRow: NewRow(RowData{\n\t\t\t\"title\":       \"AAA\",\n\t\t\t\"description\": \"\",\n\t\t}),\n\t\tFilter: \"\",\n\t}))\n\n\ttype testCase struct {\n\t\tname        string\n\t\tfilter      string\n\t\ttitle       any\n\t\tdescription any\n\t\tshouldMatch bool\n\t}\n\n\ttimeFrom2020 := time.Date(2020, time.July, 1, 1, 1, 1, 1, time.UTC)\n\n\tcases := []testCase{\n\t\t{\"empty filter matches all\", \"\", \"AAA\", \"\", true},\n\t\t{\"exact match\", \"AAA\", \"AAA\", \"\", true},\n\t\t{\"partial match start\", \"A\", \"AAA\", \"\", true},\n\t\t{\"partial match middle\", \"AA\", \"AAA\", \"\", true},\n\t\t{\"too long\", \"AAAA\", \"AAA\", \"\", false},\n\t\t{\"lowercase\", \"aaa\", \"AAA\", \"\", true},\n\t\t{\"mixed case\", \"AaA\", \"AAA\", \"\", true},\n\t\t{\"wrong input\", \"B\", \"AAA\", \"\", false},\n\t\t{\"ignore description\", \"BBB\", \"AAA\", \"BBB\", false},\n\t\t{\"time filterable success\", \"2020\", timeFrom2020, \"\", true},\n\t\t{\"time filterable wrong input\", \"2021\", timeFrom2020, \"\", false},\n\t\t{\"styled cell\", \"AAA\", NewStyledCell(\"AAA\", lipgloss.NewStyle()), \"\", true},\n\t}\n\n\tfor _, testCase := range cases {\n\t\tt.Run(testCase.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, testCase.shouldMatch, filterFuncContains(FilterFuncInput{\n\t\t\t\tColumns: columns,\n\t\t\t\tRow: NewRow(RowData{\n\t\t\t\t\t\"title\":       testCase.title,\n\t\t\t\t\t\"description\": testCase.description,\n\t\t\t\t}),\n\t\t\t\tFilter: testCase.filter,\n\t\t\t}))\n\t\t})\n\t}\n\n\t// Styled check\n}\n\nfunc TestIsRowMatchedForNonStringer(t *testing.T) {\n\tcolumns := []Column{\n\t\tNewColumn(\"val\", \"val\", 10).WithFiltered(true),\n\t}\n\n\ttype testCase struct {\n\t\tname        string\n\t\tfilter      string\n\t\tval         any\n\t\tshouldMatch bool\n\t}\n\n\tcases := []testCase{\n\t\t{\"exact match\", \"12\", 12, true},\n\t\t{\"partial match\", \"1\", 12, true},\n\t\t{\"partial match end\", \"2\", 12, true},\n\t\t{\"wrong input\", \"3\", 12, false},\n\t}\n\n\tfor _, testCase := range cases {\n\t\tt.Run(testCase.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, testCase.shouldMatch, filterFuncContains(FilterFuncInput{\n\t\t\t\tColumns: columns,\n\t\t\t\tRow: NewRow(RowData{\n\t\t\t\t\t\"val\": testCase.val,\n\t\t\t\t}),\n\t\t\t\tFilter: testCase.filter,\n\t\t\t}))\n\t\t})\n\t}\n}\n\nfunc TestGetFilteredRowsNoColumnFiltered(t *testing.T) {\n\tcolumns := []Column{NewColumn(\"title\", \"title\", 10)}\n\trows := []Row{\n\t\tNewRow(RowData{\n\t\t\t\"title\":       \"AAA\",\n\t\t\t\"description\": \"\",\n\t\t}),\n\t\tNewRow(RowData{\n\t\t\t\"title\":       \"BBB\",\n\t\t\t\"description\": \"\",\n\t\t}),\n\t\tNewRow(RowData{\n\t\t\t\"title\":       \"CCC\",\n\t\t\t\"description\": \"\",\n\t\t}),\n\t}\n\n\tmodel := New(columns).WithRows(rows).Filtered(true)\n\tmodel.filterTextInput.SetValue(\"AAA\")\n\n\tfilteredRows := model.getFilteredRows(rows)\n\n\tassert.Len(t, filteredRows, len(rows))\n}\n\nfunc TestGetFilteredRowsUnfiltered(t *testing.T) {\n\tcolumns := []Column{NewColumn(\"title\", \"title\", 10)}\n\trows := []Row{\n\t\tNewRow(RowData{\n\t\t\t\"title\": \"AAA\",\n\t\t}),\n\t\tNewRow(RowData{\n\t\t\t\"title\": \"BBB\",\n\t\t}),\n\t}\n\n\tmodel := New(columns).WithRows(rows)\n\n\tfilteredRows := model.getFilteredRows(rows)\n\n\tassert.Len(t, filteredRows, len(rows))\n}\n\nfunc TestGetFilteredRowsFiltered(t *testing.T) {\n\tcolumns := []Column{NewColumn(\"title\", \"title\", 10).WithFiltered(true)}\n\trows := []Row{\n\t\tNewRow(RowData{\n\t\t\t\"title\":       \"AAA\",\n\t\t\t\"description\": \"\",\n\t\t}),\n\t\tNewRow(RowData{\n\t\t\t\"title\":       \"BBB\",\n\t\t\t\"description\": \"\",\n\t\t}),\n\t\t// Empty\n\t\tNewRow(RowData{}),\n\t}\n\tmodel := New(columns).WithRows(rows).Filtered(true)\n\tmodel.filterTextInput.SetValue(\"AaA\")\n\n\tfilteredRows := model.getFilteredRows(rows)\n\n\tassert.Len(t, filteredRows, 1)\n}\n\nfunc TestGetFilteredRowsRefocusAfterFilter(t *testing.T) {\n\tcolumns := []Column{NewColumn(\"title\", \"title\", 10).WithFiltered(true)}\n\trows := []Row{\n\t\tNewRow(RowData{\n\t\t\t\"title\":       \"a\",\n\t\t\t\"description\": \"\",\n\t\t}),\n\t\tNewRow(RowData{\n\t\t\t\"title\":       \"b\",\n\t\t\t\"description\": \"\",\n\t\t}),\n\t\tNewRow(RowData{\n\t\t\t\"title\":       \"c\",\n\t\t\t\"description\": \"\",\n\t\t}),\n\t\tNewRow(RowData{\n\t\t\t\"title\":       \"d1\",\n\t\t\t\"description\": \"\",\n\t\t}),\n\t\tNewRow(RowData{\n\t\t\t\"title\":       \"d2\",\n\t\t\t\"description\": \"\",\n\t\t}),\n\t}\n\tmodel := New(columns).WithRows(rows).Filtered(true).WithPageSize(1)\n\tmodel = model.PageDown()\n\tassert.Len(t, model.GetVisibleRows(), 5)\n\tassert.Equal(t, 1, model.PageSize())\n\tassert.Equal(t, 2, model.CurrentPage())\n\tassert.Equal(t, 5, model.MaxPages())\n\tassert.Equal(t, 5, model.TotalRows())\n\n\tmodel.filterTextInput.SetValue(\"c\")\n\tmodel, _ = model.updateFilterTextInput(tea.KeyMsg{})\n\tassert.Len(t, model.GetVisibleRows(), 1)\n\tassert.Equal(t, 1, model.PageSize())\n\tassert.Equal(t, 1, model.CurrentPage())\n\tassert.Equal(t, 1, model.MaxPages())\n\tassert.Equal(t, 1, model.TotalRows())\n\n\tmodel.filterTextInput.SetValue(\"not-exist\")\n\tmodel, _ = model.updateFilterTextInput(tea.KeyMsg{})\n\tassert.Len(t, model.GetVisibleRows(), 0)\n\tassert.Equal(t, 1, model.PageSize())\n\tassert.Equal(t, 1, model.CurrentPage())\n\tassert.Equal(t, 1, model.MaxPages())\n\tassert.Equal(t, 0, model.TotalRows())\n}\n\nfunc TestFilterWithExternalTextInput(t *testing.T) {\n\tcolumns := []Column{NewColumn(\"title\", \"title\", 10).WithFiltered(true)}\n\trows := []Row{\n\t\tNewRow(RowData{\n\t\t\t\"title\":       \"AAA\",\n\t\t\t\"description\": \"\",\n\t\t}),\n\t\tNewRow(RowData{\n\t\t\t\"title\":       \"BBB\",\n\t\t\t\"description\": \"\",\n\t\t}),\n\t\t// Empty\n\t\tNewRow(RowData{}),\n\t}\n\n\t// Page size 1 to test scrolling back if input changes\n\tmodel := New(columns).WithRows(rows).Filtered(true).WithPageSize(1)\n\tmodel.pageDown()\n\tassert.Equal(t, 2, model.CurrentPage(), \"Should start on second page for test\")\n\tinput := textinput.New()\n\tinput.SetValue(\"AaA\")\n\tmodel = model.WithFilterInput(input)\n\tassert.Equal(t, 1, model.CurrentPage(), \"Did not go back to first page\")\n\n\tfilteredRows := model.getFilteredRows(rows)\n\n\tassert.Len(t, filteredRows, 1)\n}\n\nfunc TestFilterWithSetValue(t *testing.T) {\n\tcolumns := []Column{NewColumn(\"title\", \"title\", 10).WithFiltered(true)}\n\trows := []Row{\n\t\tNewRow(RowData{\n\t\t\t\"title\":       \"AAA\",\n\t\t\t\"description\": \"\",\n\t\t}),\n\t\tNewRow(RowData{\n\t\t\t\"title\":       \"BBB\",\n\t\t\t\"description\": \"\",\n\t\t}),\n\t\t// Empty\n\t\tNewRow(RowData{}),\n\t}\n\n\t// Page size 1 to make sure we scroll back correctly\n\tmodel := New(columns).WithRows(rows).Filtered(true).WithPageSize(1)\n\tmodel.pageDown()\n\tassert.Equal(t, 2, model.CurrentPage(), \"Should start on second page for test\")\n\tmodel = model.WithFilterInputValue(\"AaA\")\n\n\tassert.Equal(t, 1, model.CurrentPage(), \"Did not go back to first page\")\n\n\tfilteredRows := model.getFilteredRows(rows)\n\tassert.Len(t, filteredRows, 1)\n\n\t// Make sure it holds true after an update\n\tmodel, _ = model.Update(tea.KeyRight)\n\tfilteredRows = model.getFilteredRows(rows)\n\tassert.Len(t, filteredRows, 1)\n\n\t// Remove filter\n\tmodel = model.WithFilterInputValue(\"\")\n\tfilteredRows = model.getFilteredRows(rows)\n\tassert.Len(t, filteredRows, 3)\n}\n\nfunc TestFilterFunc(t *testing.T) {\n\tconst (\n\t\tcolTitle = \"title\"\n\t\tcolDesc  = \"description\"\n\t)\n\n\tcolumns := []Column{NewColumn(\"title\", \"title\", 10).WithFiltered(true)}\n\trows := []Row{\n\t\tNewRow(RowData{\n\t\t\tcolTitle: \"AAA\",\n\t\t\tcolDesc:  \"\",\n\t\t}),\n\t\tNewRow(RowData{\n\t\t\tcolTitle: \"BBB\",\n\t\t\tcolDesc:  \"\",\n\t\t}),\n\t\t// Empty\n\t\tNewRow(RowData{}),\n\t}\n\n\tfilterFunc := func(input FilterFuncInput) bool {\n\t\t// Completely arbitrary check for testing purposes\n\t\ttitle := fmt.Sprintf(\"%v\", input.Row.Data[\"title\"])\n\n\t\treturn title == \"AAA\" && input.Filter == \"x\" && input.GlobalMetadata[\"testValue\"] == 3\n\t}\n\n\t// First check that the table won't match with different case\n\tmodel := New(columns).WithRows(rows).Filtered(true).WithGlobalMetadata(map[string]any{\n\t\t\"testValue\": 3,\n\t})\n\tmodel = model.WithFilterInputValue(\"x\")\n\n\tfilteredRows := model.getFilteredRows(rows)\n\tassert.Len(t, filteredRows, 0)\n\n\t// The filter func should then match the one row\n\tmodel = model.WithFilterFunc(filterFunc)\n\tfilteredRows = model.getFilteredRows(rows)\n\tassert.Len(t, filteredRows, 1)\n\n\t// Remove filter\n\tmodel = model.WithFilterInputValue(\"\")\n\tfilteredRows = model.getFilteredRows(rows)\n\tassert.Len(t, filteredRows, 3)\n}\n\nfunc BenchmarkFilteredScrolling(b *testing.B) {\n\t// Scrolling through a filtered table with many rows should be quick\n\t// https://github.com/Evertras/bubble-table/issues/135\n\tconst rowCount = 40000\n\tcolumns := []Column{NewColumn(\"title\", \"title\", 10).WithFiltered(true)}\n\trows := make([]Row, rowCount)\n\n\tfor i := 0; i < rowCount; i++ {\n\t\trows[i] = NewRow(RowData{\n\t\t\t\"title\": fmt.Sprintf(\"%d\", i),\n\t\t})\n\t}\n\n\tmodel := New(columns).WithRows(rows).Filtered(true)\n\tmodel = model.WithFilterInputValue(\"1\")\n\n\thitKey := func(key rune) {\n\t\tmodel, _ = model.Update(\n\t\t\ttea.KeyMsg{\n\t\t\t\tType:  tea.KeyRunes,\n\t\t\t\tRunes: []rune{key},\n\t\t\t})\n\t}\n\n\tb.ResetTimer()\n\n\tfor i := 0; i < b.N; i++ {\n\t\thitKey('j')\n\t}\n}\n\nfunc BenchmarkFilteredScrollingPaged(b *testing.B) {\n\t// Scrolling through a filtered table with many rows should be quick\n\t// https://github.com/Evertras/bubble-table/issues/135\n\tconst rowCount = 40000\n\tcolumns := []Column{NewColumn(\"title\", \"title\", 10).WithFiltered(true)}\n\trows := make([]Row, rowCount)\n\n\tfor i := 0; i < rowCount; i++ {\n\t\trows[i] = NewRow(RowData{\n\t\t\t\"title\": fmt.Sprintf(\"%d\", i),\n\t\t})\n\t}\n\n\tmodel := New(columns).WithRows(rows).Filtered(true).WithPageSize(50)\n\tmodel = model.WithFilterInputValue(\"1\")\n\n\tb.ResetTimer()\n\n\tfor i := 0; i < b.N; i++ {\n\t\tmodel, _ = model.Update(\n\t\t\ttea.KeyMsg{\n\t\t\t\tType:  tea.KeyRunes,\n\t\t\t\tRunes: []rune{'j'},\n\t\t\t})\n\t}\n}\n\nfunc BenchmarkFilteredRenders(b *testing.B) {\n\t// Rendering a filtered table should be fast\n\t// https://github.com/Evertras/bubble-table/issues/135\n\tconst rowCount = 40000\n\tcolumns := []Column{NewColumn(\"title\", \"title\", 10).WithFiltered(true)}\n\trows := make([]Row, rowCount)\n\n\tfor i := 0; i < rowCount; i++ {\n\t\trows[i] = NewRow(RowData{\n\t\t\t\"title\": fmt.Sprintf(\"%d\", i),\n\t\t})\n\t}\n\n\tmodel := New(columns).WithRows(rows).Filtered(true).WithPageSize(50)\n\tmodel = model.WithFilterInputValue(\"1\")\n\n\tb.ResetTimer()\n\n\tfor i := 0; i < b.N; i++ {\n\t\t// Don't care about result, just rendering\n\t\t_ = model.View()\n\t}\n}\n\nfunc TestFuzzyFilter_EmptyFilterMatchesAll(t *testing.T) {\n\tcols := []Column{\n\t\tNewColumn(\"name\", \"Name\", 10).WithFiltered(true),\n\t}\n\trows := []Row{\n\t\tNewRow(RowData{\"name\": \"Acme Steel\"}),\n\t\tNewRow(RowData{\"name\": \"Globex\"}),\n\t}\n\n\tfor index, row := range rows {\n\t\tif !filterFuncFuzzy(FilterFuncInput{\n\t\t\tColumns: cols,\n\t\t\tRow:     row,\n\t\t\tFilter:  \"\",\n\t\t}) {\n\t\t\tt.Fatalf(\"row %d should match empty filter\", index)\n\t\t}\n\t}\n}\n\nfunc TestFuzzyFilter_SubsequenceAcrossColumns(t *testing.T) {\n\tcols := []Column{\n\t\tNewColumn(\"name\", \"Name\", 10).WithFiltered(true),\n\t\tNewColumn(\"city\", \"City\", 10).WithFiltered(true),\n\t}\n\trow := NewRow(RowData{\n\t\t\"name\": \"Acme\",\n\t\t\"city\": \"Stuttgart\",\n\t})\n\n\ttype testCase struct {\n\t\tname        string\n\t\tfilter      string\n\t\tshouldMatch bool\n\t}\n\n\ttestCases := []testCase{\n\t\t{\"subsequence match\", \"agt\", true},\n\t\t{\"case-insensitive match\", \"ACM\", true},\n\t\t{\"not a subsequence\", \"zzt\", false},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tassert.Equal(t, tc.shouldMatch, filterFuncFuzzy(FilterFuncInput{\n\t\t\tColumns: cols,\n\t\t\tRow:     row,\n\t\t\tFilter:  tc.filter,\n\t\t}))\n\t}\n}\n\nfunc TestFuzzyFilter_ColumnNotInRow(t *testing.T) {\n\tcols := []Column{\n\t\tNewColumn(\"column_name_doesnt_match\", \"Name\", 10).WithFiltered(true),\n\t}\n\trow := NewRow(RowData{\n\t\t\"name\": \"Acme Steel\",\n\t})\n\n\tassert.False(t, filterFuncFuzzy(FilterFuncInput{\n\t\tColumns: cols,\n\t\tRow:     row,\n\t\tFilter:  \"steel\",\n\t}), \"Shouldn't match\")\n}\n\nfunc TestFuzzyFilter_RowHasEmptyHaystack(t *testing.T) {\n\tcols := []Column{\n\t\tNewColumn(\"name\", \"Name\", 10).WithFiltered(true),\n\t}\n\trow := NewRow(RowData{\"name\": \"\"})\n\n\t// literally any value other than an empty string\n\t// should not match\n\tassert.False(t, filterFuncFuzzy(FilterFuncInput{\n\t\tColumns: cols,\n\t\tRow:     row,\n\t\tFilter:  \"a\",\n\t}), \"Shouldn't match\")\n}\n\nfunc TestFuzzyFilter_MultiToken_AND(t *testing.T) {\n\tcols := []Column{\n\t\tNewColumn(\"name\", \"Name\", 10).WithFiltered(true),\n\t\tNewColumn(\"dept\", \"Dept\", 10).WithFiltered(true),\n\t}\n\trow := NewRow(RowData{\n\t\t\"name\": \"Wayne Enterprises\",\n\t\t\"dept\": \"R&D\",\n\t})\n\n\t// Both tokens must match as subsequences somewhere in the concatenated haystack\n\tassert.True(t, filterFuncFuzzy(FilterFuncInput{\n\t\tColumns: cols,\n\t\tRow:     row,\n\t\tFilter:  \"wy ent\",\n\t}), \"Should match wy ent\") // \"wy\" in Wayne, \"ent\" in Enterprises\n\tassert.False(t, filterFuncFuzzy(FilterFuncInput{\n\t\tColumns: cols,\n\t\tRow:     row,\n\t\tFilter:  \"wy zzz\",\n\t}), \"Shouldn't match wy zzz\")\n}\n\nfunc TestFuzzyFilter_IgnoresNonFilterableColumns(t *testing.T) {\n\tcols := []Column{\n\t\tNewColumn(\"name\", \"Name\", 10).WithFiltered(true),\n\t\tNewColumn(\"secret\", \"Secret\", 10).WithFiltered(false), // should be ignored\n\t}\n\trow := NewRow(RowData{\n\t\t\"name\":   \"Acme\",\n\t\t\"secret\": \"topsecretpattern\",\n\t})\n\n\tassert.False(t, filterFuncFuzzy(FilterFuncInput{\n\t\tColumns: cols,\n\t\tRow:     row,\n\t\tFilter:  \"topsecret\",\n\t}), \"Shouldn't match on non-filterable\")\n}\n\nfunc TestFuzzyFilter_UnwrapsStyledCell(t *testing.T) {\n\tcols := []Column{\n\t\tNewColumn(\"name\", \"Name\", 10).WithFiltered(true),\n\t}\n\trow := NewRow(RowData{\n\t\t\"name\": NewStyledCell(\"Nakatomi Plaza\", lipgloss.NewStyle()),\n\t})\n\n\tassert.True(t, filterFuncFuzzy(FilterFuncInput{\n\t\tColumns: cols,\n\t\tRow:     row,\n\t\tFilter:  \"nak plz\",\n\t}), \"Expected fuzzy subsequence to match within StyledCell data\")\n}\n\nfunc TestFuzzyFilter_NonStringValuesFormatted(t *testing.T) {\n\tcols := []Column{\n\t\tNewColumn(\"id\", \"ID\", 6).WithFiltered(true),\n\t}\n\trow := NewRow(RowData{\n\t\t\"id\": 12345, // should be formatted via fmt.Sprintf(\"%v\", v)\n\t})\n\n\tassert.True(t, filterFuncFuzzy(FilterFuncInput{\n\t\tColumns: cols,\n\t\tRow:     row,\n\t\tFilter:  \"245\", // subsequence of \"12345\"\n\t}), \"expected matcher to format non-strings and match subsequence\")\n}\n\nfunc TestFuzzySubSequenceMatch_EmptyString(t *testing.T) {\n\tassert.True(t, fuzzySubsequenceMatch(\"anything\", \"\"), \"empty needle should match anything\")\n\tassert.False(t, fuzzySubsequenceMatch(\"\", \"a\"), \"non-empty needle should not match empty haystack\")\n\tassert.True(t, fuzzySubsequenceMatch(\"\", \"\"), \"empty needle should match empty haystack\")\n}\n"
  },
  {
    "path": "table/footer.go",
    "content": "package table\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\nfunc (m Model) hasFooter() bool {\n\treturn m.footerVisible && (m.staticFooter != \"\" || m.pageSize != 0 || m.filtered)\n}\n\nfunc (m Model) renderFooter(width int, includeTop bool) string {\n\tif !m.hasFooter() {\n\t\treturn \"\"\n\t}\n\n\tconst borderAdjustment = 2\n\n\tstyleFooter := m.baseStyle.Copy().Inherit(m.border.styleFooter).Width(width - borderAdjustment)\n\n\tif includeTop {\n\t\tstyleFooter = styleFooter.BorderTop(true)\n\t}\n\n\tif m.staticFooter != \"\" {\n\t\treturn styleFooter.Render(m.staticFooter)\n\t}\n\n\tsections := []string{}\n\n\tif m.filtered && (m.filterTextInput.Focused() || m.filterTextInput.Value() != \"\") {\n\t\tsections = append(sections, m.filterTextInput.View())\n\t}\n\n\t// paged feature enabled\n\tif m.pageSize != 0 {\n\t\tstr := fmt.Sprintf(\"%d/%d\", m.CurrentPage(), m.MaxPages())\n\t\tif m.filtered && m.filterTextInput.Focused() {\n\t\t\t// Need to apply inline style here in case of filter input cursor, because\n\t\t\t// the input cursor resets the style after rendering.  Note that Inline(true)\n\t\t\t// creates a copy, so it's safe to use here without mutating the underlying\n\t\t\t// base style.\n\t\t\tstr = m.baseStyle.Inline(true).Render(str)\n\t\t}\n\t\tsections = append(sections, str)\n\t}\n\n\tfooterText := strings.Join(sections, \" \")\n\n\treturn styleFooter.Render(footerText)\n}\n"
  },
  {
    "path": "table/header.go",
    "content": "package table\n\nimport \"github.com/charmbracelet/lipgloss\"\n\n// This is long and could use some refactoring in the future, but unsure of how\n// to pick it apart right now.\n//\n//nolint:funlen,cyclop\nfunc (m Model) renderHeaders() string {\n\theaderStrings := []string{}\n\n\ttotalRenderedWidth := 0\n\n\theaderStyles := m.styleHeaders()\n\n\trenderHeader := func(column Column, borderStyle lipgloss.Style) string {\n\t\tborderStyle = borderStyle.Inherit(column.style).Inherit(m.baseStyle)\n\n\t\theaderSection := limitStr(column.title, column.width)\n\n\t\treturn borderStyle.Render(headerSection)\n\t}\n\n\tfor columnIndex, column := range m.columns {\n\t\tvar borderStyle lipgloss.Style\n\n\t\tif m.horizontalScrollOffsetCol > 0 && columnIndex == m.horizontalScrollFreezeColumnsCount {\n\t\t\tif columnIndex == 0 {\n\t\t\t\tborderStyle = headerStyles.left.Copy()\n\t\t\t} else {\n\t\t\t\tborderStyle = headerStyles.inner.Copy()\n\t\t\t}\n\n\t\t\trendered := renderHeader(genOverflowColumnLeft(1), borderStyle)\n\n\t\t\ttotalRenderedWidth += lipgloss.Width(rendered)\n\n\t\t\theaderStrings = append(headerStrings, rendered)\n\t\t}\n\n\t\tif columnIndex >= m.horizontalScrollFreezeColumnsCount &&\n\t\t\tcolumnIndex < m.horizontalScrollOffsetCol+m.horizontalScrollFreezeColumnsCount {\n\t\t\tcontinue\n\t\t}\n\n\t\tif len(headerStrings) == 0 {\n\t\t\tborderStyle = headerStyles.left.Copy()\n\t\t} else if columnIndex < len(m.columns)-1 {\n\t\t\tborderStyle = headerStyles.inner.Copy()\n\t\t} else {\n\t\t\tborderStyle = headerStyles.right.Copy()\n\t\t}\n\n\t\trendered := renderHeader(column, borderStyle)\n\n\t\tif m.maxTotalWidth != 0 {\n\t\t\trenderedWidth := lipgloss.Width(rendered)\n\n\t\t\tconst (\n\t\t\t\tborderAdjustment = 1\n\t\t\t\toverflowColWidth = 2\n\t\t\t)\n\n\t\t\ttargetWidth := m.maxTotalWidth - overflowColWidth\n\n\t\t\tif columnIndex == len(m.columns)-1 {\n\t\t\t\t// If this is the last header, we don't need to account for the\n\t\t\t\t// overflow arrow column\n\t\t\t\ttargetWidth = m.maxTotalWidth\n\t\t\t}\n\n\t\t\tif totalRenderedWidth+renderedWidth > targetWidth {\n\t\t\t\toverflowWidth := m.maxTotalWidth - totalRenderedWidth - borderAdjustment\n\t\t\t\toverflowStyle := genOverflowStyle(headerStyles.right, overflowWidth)\n\t\t\t\toverflowColumn := genOverflowColumnRight(overflowWidth)\n\n\t\t\t\toverflowStr := renderHeader(overflowColumn, overflowStyle)\n\n\t\t\t\theaderStrings = append(headerStrings, overflowStr)\n\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\ttotalRenderedWidth += renderedWidth\n\t\t}\n\n\t\theaderStrings = append(headerStrings, rendered)\n\t}\n\n\theaderBlock := lipgloss.JoinHorizontal(lipgloss.Bottom, headerStrings...)\n\n\treturn headerBlock\n}\n"
  },
  {
    "path": "table/keys.go",
    "content": "package table\n\nimport \"github.com/charmbracelet/bubbles/key\"\n\n// KeyMap defines the keybindings for the table when it's focused.\ntype KeyMap struct {\n\tRowDown key.Binding\n\tRowUp   key.Binding\n\n\tRowSelectToggle key.Binding\n\n\tPageDown  key.Binding\n\tPageUp    key.Binding\n\tPageFirst key.Binding\n\tPageLast  key.Binding\n\n\t// Filter allows the user to start typing and filter the rows.\n\tFilter key.Binding\n\n\t// FilterBlur is the key that stops the user's input from typing into the filter.\n\tFilterBlur key.Binding\n\n\t// FilterClear will clear the filter while it's blurred.\n\tFilterClear key.Binding\n\n\t// ScrollRight will move one column to the right when overflow occurs.\n\tScrollRight key.Binding\n\n\t// ScrollLeft will move one column to the left when overflow occurs.\n\tScrollLeft key.Binding\n}\n\n// DefaultKeyMap returns a set of sensible defaults for controlling a focused table with help text.\nfunc DefaultKeyMap() KeyMap {\n\treturn KeyMap{\n\t\tRowDown: key.NewBinding(\n\t\t\tkey.WithKeys(\"down\", \"j\"),\n\t\t\tkey.WithHelp(\"↓/j\", \"move down\"),\n\t\t),\n\t\tRowUp: key.NewBinding(\n\t\t\tkey.WithKeys(\"up\", \"k\"),\n\t\t\tkey.WithHelp(\"↑/k\", \"move up\"),\n\t\t),\n\t\tRowSelectToggle: key.NewBinding(\n\t\t\tkey.WithKeys(\" \", \"enter\"),\n\t\t\tkey.WithHelp(\"<space>/enter\", \"select row\"),\n\t\t),\n\t\tPageDown: key.NewBinding(\n\t\t\tkey.WithKeys(\"right\", \"l\", \"pgdown\"),\n\t\t\tkey.WithHelp(\"→/h/page down\", \"next page\"),\n\t\t),\n\t\tPageUp: key.NewBinding(\n\t\t\tkey.WithKeys(\"left\", \"h\", \"pgup\"),\n\t\t\tkey.WithHelp(\"←/h/page up\", \"previous page\"),\n\t\t),\n\t\tPageFirst: key.NewBinding(\n\t\t\tkey.WithKeys(\"home\", \"g\"),\n\t\t\tkey.WithHelp(\"home/g\", \"first page\"),\n\t\t),\n\t\tPageLast: key.NewBinding(\n\t\t\tkey.WithKeys(\"end\", \"G\"),\n\t\t\tkey.WithHelp(\"end/G\", \"last page\"),\n\t\t),\n\t\tFilter: key.NewBinding(\n\t\t\tkey.WithKeys(\"/\"),\n\t\t\tkey.WithHelp(\"/\", \"filter\"),\n\t\t),\n\t\tFilterBlur: key.NewBinding(\n\t\t\tkey.WithKeys(\"enter\", \"esc\"),\n\t\t\tkey.WithHelp(\"enter/esc\", \"unfocus\"),\n\t\t),\n\t\tFilterClear: key.NewBinding(\n\t\t\tkey.WithKeys(\"esc\"),\n\t\t\tkey.WithHelp(\"esc\", \"clear filter\"),\n\t\t),\n\t\tScrollRight: key.NewBinding(\n\t\t\tkey.WithKeys(\"shift+right\"),\n\t\t\tkey.WithHelp(\"shift+→\", \"scroll right\"),\n\t\t),\n\t\tScrollLeft: key.NewBinding(\n\t\t\tkey.WithKeys(\"shift+left\"),\n\t\t\tkey.WithHelp(\"shift+←\", \"scroll left\"),\n\t\t),\n\t}\n}\n\n// FullHelp returns a multi row view of all the helpkeys that are defined. Needed to fullfil the 'help.Model' interface.\n// Also appends all user defined extra keys to the help.\nfunc (m Model) FullHelp() [][]key.Binding {\n\tkeyBinds := [][]key.Binding{\n\t\t{m.keyMap.RowDown, m.keyMap.RowUp, m.keyMap.RowSelectToggle},\n\t\t{m.keyMap.PageDown, m.keyMap.PageUp, m.keyMap.PageFirst, m.keyMap.PageLast},\n\t\t{m.keyMap.Filter, m.keyMap.FilterBlur, m.keyMap.FilterClear, m.keyMap.ScrollRight, m.keyMap.ScrollLeft},\n\t}\n\tif m.additionalFullHelpKeys != nil {\n\t\tkeyBinds = append(keyBinds, m.additionalFullHelpKeys())\n\t}\n\n\treturn keyBinds\n}\n\n// ShortHelp just returns a single row of help views. Needed to fullfil the 'help.Model' interface.\n// Also appends all user defined extra keys to the help.\nfunc (m Model) ShortHelp() []key.Binding {\n\tkeyBinds := []key.Binding{\n\t\tm.keyMap.RowDown,\n\t\tm.keyMap.RowUp,\n\t\tm.keyMap.RowSelectToggle,\n\t\tm.keyMap.PageDown,\n\t\tm.keyMap.PageUp,\n\t\tm.keyMap.Filter,\n\t\tm.keyMap.FilterBlur,\n\t\tm.keyMap.FilterClear,\n\t}\n\tif m.additionalShortHelpKeys != nil {\n\t\tkeyBinds = append(keyBinds, m.additionalShortHelpKeys()...)\n\t}\n\n\treturn keyBinds\n}\n"
  },
  {
    "path": "table/keys_test.go",
    "content": "package table\n\nimport (\n\t\"testing\"\n\n\t\"github.com/charmbracelet/bubbles/help\"\n\t\"github.com/charmbracelet/bubbles/key\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestKeyMapShortHelp(t *testing.T) {\n\tcolumns := []Column{NewColumn(\"c1\", \"Column1\", 10)}\n\tmodel := New(columns)\n\tkm := DefaultKeyMap()\n\tmodel.WithKeyMap(km)\n\tassert.Nil(t, model.additionalShortHelpKeys)\n\tassert.Equal(t, model.ShortHelp(), []key.Binding{\n\t\tmodel.keyMap.RowDown,\n\t\tmodel.keyMap.RowUp,\n\t\tmodel.keyMap.RowSelectToggle,\n\t\tmodel.keyMap.PageDown,\n\t\tmodel.keyMap.PageUp,\n\t\tmodel.keyMap.Filter,\n\t\tmodel.keyMap.FilterBlur,\n\t\tmodel.keyMap.FilterClear},\n\t)\n\t// Testing if the 'adding of keys' works too.\n\tkeys := []key.Binding{key.NewBinding(key.WithKeys(\"t\"), key.WithHelp(\"t\", \"Testing additional keybinds\"))}\n\tmodel = model.WithAdditionalShortHelpKeys(keys)\n\tassert.NotNil(t, model.additionalShortHelpKeys)\n\tassert.Equal(t, model.ShortHelp(), []key.Binding{\n\t\tmodel.keyMap.RowDown,\n\t\tmodel.keyMap.RowUp,\n\t\tmodel.keyMap.RowSelectToggle,\n\t\tmodel.keyMap.PageDown,\n\t\tmodel.keyMap.PageUp,\n\t\tmodel.keyMap.Filter,\n\t\tmodel.keyMap.FilterBlur,\n\t\tmodel.keyMap.FilterClear,\n\t\tkey.NewBinding(\n\t\t\tkey.WithKeys(\"t\"),\n\t\t\tkey.WithHelp(\"t\",\n\t\t\t\t\"Testing additional keybinds\"),\n\t\t),\n\t})\n}\n\nfunc TestKeyMapFullHelp(t *testing.T) {\n\tcolumns := []Column{NewColumn(\"c1\", \"Column1\", 10)}\n\tmodel := New(columns)\n\tkm := DefaultKeyMap()\n\tmodel.WithKeyMap(km)\n\tassert.Nil(t, model.additionalFullHelpKeys)\n\tassert.Equal(t,\n\t\tmodel.FullHelp(),\n\t\t[][]key.Binding{\n\t\t\t{model.keyMap.RowDown, model.keyMap.RowUp, model.keyMap.RowSelectToggle},\n\t\t\t{model.keyMap.PageDown, model.keyMap.PageUp, model.keyMap.PageFirst, model.keyMap.PageLast},\n\t\t\t{\n\t\t\t\tmodel.keyMap.Filter,\n\t\t\t\tmodel.keyMap.FilterBlur,\n\t\t\t\tmodel.keyMap.FilterClear,\n\t\t\t\tmodel.keyMap.ScrollRight,\n\t\t\t\tmodel.keyMap.ScrollLeft,\n\t\t\t},\n\t\t},\n\t)\n\t// Testing if the 'adding of keys' works too.\n\tkeys := []key.Binding{key.NewBinding(key.WithKeys(\"t\"), key.WithHelp(\"t\", \"Testing additional keybinds\"))}\n\tmodel = model.WithAdditionalFullHelpKeys(keys)\n\tassert.NotNil(t, model.additionalFullHelpKeys)\n\tassert.Equal(t,\n\t\tmodel.FullHelp(),\n\t\t[][]key.Binding{\n\t\t\t{model.keyMap.RowDown, model.keyMap.RowUp, model.keyMap.RowSelectToggle},\n\t\t\t{model.keyMap.PageDown, model.keyMap.PageUp, model.keyMap.PageFirst, model.keyMap.PageLast},\n\t\t\t{model.keyMap.Filter, model.keyMap.FilterBlur,\n\t\t\t\tmodel.keyMap.FilterClear,\n\t\t\t\tmodel.keyMap.ScrollRight,\n\t\t\t\tmodel.keyMap.ScrollLeft},\n\t\t\t{key.NewBinding(key.WithKeys(\"t\"), key.WithHelp(\"t\", \"Testing additional keybinds\"))}},\n\t)\n}\n\n// Testing if Model actually implements the 'help.KeyMap' interface.\nfunc TestKeyMapInterface(t *testing.T) {\n\tmodel := New(nil)\n\tassert.Implements(t, (*help.KeyMap)(nil), model)\n}\n"
  },
  {
    "path": "table/model.go",
    "content": "package table\n\nimport (\n\t\"github.com/charmbracelet/bubbles/key\"\n\t\"github.com/charmbracelet/bubbles/textinput\"\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/charmbracelet/lipgloss\"\n)\n\nconst (\n\tcolumnKeySelect = \"___select___\"\n)\n\nvar (\n\tdefaultHighlightStyle = lipgloss.NewStyle().Background(lipgloss.Color(\"#334\"))\n)\n\n// Model is the main table model.  Create using New().\ntype Model struct {\n\t// Data\n\tcolumns  []Column\n\trows     []Row\n\tmetadata map[string]any\n\n\t// Caches for optimizations\n\tvisibleRowCacheUpdated bool\n\tvisibleRowCache        []Row\n\n\t// Shown when data is missing from a row\n\tmissingDataIndicator any\n\n\t// Interaction\n\tfocused bool\n\tkeyMap  KeyMap\n\n\t// Taken from: 'Bubbles/List'\n\t// Additional key mappings for the short and full help views. This allows\n\t// you to add additional key mappings to the help menu without\n\t// re-implementing the help component. Of course, you can also disable the\n\t// list's help component and implement a new one if you need more\n\t// flexibility.\n\t// You have to supply a keybinding like this:\n\t// key.NewBinding( key.WithKeys(\"shift+left\"), key.WithHelp(\"shift+←\", \"scroll left\"))\n\t// It needs both 'WithKeys' and 'WithHelp'\n\tadditionalShortHelpKeys func() []key.Binding\n\tadditionalFullHelpKeys  func() []key.Binding\n\n\tselectableRows bool\n\trowCursorIndex int\n\n\t// Events\n\tlastUpdateUserEvents []UserEvent\n\n\t// Styles\n\tbaseStyle      lipgloss.Style\n\thighlightStyle lipgloss.Style\n\theaderStyle    lipgloss.Style\n\trowStyleFunc   func(RowStyleFuncInput) lipgloss.Style\n\tborder         Border\n\tselectedText   string\n\tunselectedText string\n\n\t// Header\n\theaderVisible bool\n\n\t// Footers\n\tfooterVisible bool\n\tstaticFooter  string\n\n\t// Pagination\n\tpageSize           int\n\tcurrentPage        int\n\tpaginationWrapping bool\n\n\t// Sorting, where a stable sort is applied from first element to last so\n\t// that elements are grouped by the later elements.\n\tsortOrder []SortColumn\n\n\t// Filter\n\tfiltered        bool\n\tfilterTextInput textinput.Model\n\tfilterFunc      FilterFunc\n\n\t// For flex columns\n\ttargetTotalWidth int\n\n\t// The maximum total width for overflow/scrolling\n\tmaxTotalWidth int\n\n\t// Internal cached calculations for reference, may be higher than\n\t// maxTotalWidth.  If this is the case, we need to adjust the view\n\ttotalWidth int\n\n\t// How far to scroll to the right, in columns\n\thorizontalScrollOffsetCol int\n\n\t// How many columns to freeze when scrolling horizontally\n\thorizontalScrollFreezeColumnsCount int\n\n\t// Calculated maximum column we can scroll to before the last is displayed\n\tmaxHorizontalColumnIndex int\n\n\t// Minimum total height of the table\n\tminimumHeight int\n\n\t// Internal cached calculation, the height of the header and footer\n\t// including borders. Used to determine how many padding rows to add.\n\tmetaHeight int\n\n\t// If true, the table will be multiline\n\tmultiline bool\n}\n\n// New creates a new table ready for further modifications.\nfunc New(columns []Column) Model {\n\tfilterInput := textinput.New()\n\tfilterInput.Prompt = \"/\"\n\tmodel := Model{\n\t\tcolumns:        make([]Column, len(columns)),\n\t\tmetadata:       make(map[string]any),\n\t\thighlightStyle: defaultHighlightStyle.Copy(),\n\t\tborder:         borderDefault,\n\t\theaderVisible:  true,\n\t\tfooterVisible:  true,\n\t\tkeyMap:         DefaultKeyMap(),\n\n\t\tselectedText:   \"[x]\",\n\t\tunselectedText: \"[ ]\",\n\n\t\tfilterTextInput: filterInput,\n\t\tfilterFunc:      filterFuncContains,\n\t\tbaseStyle:       lipgloss.NewStyle().Align(lipgloss.Right),\n\n\t\tpaginationWrapping: true,\n\t}\n\n\t// Do a full deep copy to avoid unexpected edits\n\tcopy(model.columns, columns)\n\n\tmodel.recalculateWidth()\n\n\treturn model\n}\n\n// Init initializes the table per the Bubble Tea architecture.\nfunc (m Model) Init() tea.Cmd {\n\treturn nil\n}\n"
  },
  {
    "path": "table/model_test.go",
    "content": "package table\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestModelInitReturnsNil(t *testing.T) {\n\tmodel := New(nil)\n\n\tcmd := model.Init()\n\n\tassert.Nil(t, cmd)\n}\n"
  },
  {
    "path": "table/options.go",
    "content": "package table\n\nimport (\n\t\"github.com/charmbracelet/bubbles/key\"\n\t\"github.com/charmbracelet/bubbles/textinput\"\n\t\"github.com/charmbracelet/lipgloss\"\n)\n\n// RowStyleFuncInput is the input to the style function that can\n// be applied to each row.  This is useful for things like zebra\n// striping or other data-based styles.\n//\n// Note that we use a struct here to allow for future expansion\n// while keeping backwards compatibility.\ntype RowStyleFuncInput struct {\n\t// Index is the index of the row, starting at 0.\n\tIndex int\n\n\t// Row is the full row data.\n\tRow Row\n\n\t// IsHighlighted is true if the row is currently highlighted.\n\tIsHighlighted bool\n}\n\n// WithRowStyleFunc sets a function that can be used to apply a style to each row\n// based on the row data.  This is useful for things like zebra striping or other\n// data-based styles.  It can be safely set to nil to remove it later.\n// This style is applied after the base style and before individual row styles.\n// This will override any HighlightStyle settings.\nfunc (m Model) WithRowStyleFunc(f func(RowStyleFuncInput) lipgloss.Style) Model {\n\tm.rowStyleFunc = f\n\n\treturn m\n}\n\n// WithHighlightedRow sets the highlighted row to the given index.\nfunc (m Model) WithHighlightedRow(index int) Model {\n\tm.rowCursorIndex = index\n\n\tif m.rowCursorIndex >= len(m.GetVisibleRows()) {\n\t\tm.rowCursorIndex = len(m.GetVisibleRows()) - 1\n\t}\n\n\tif m.rowCursorIndex < 0 {\n\t\tm.rowCursorIndex = 0\n\t}\n\n\tm.currentPage = m.expectedPageForRowIndex(m.rowCursorIndex)\n\n\treturn m\n}\n\n// HeaderStyle sets the style to apply to the header text, such as color or bold.\nfunc (m Model) HeaderStyle(style lipgloss.Style) Model {\n\tm.headerStyle = style.Copy()\n\n\treturn m\n}\n\n// WithRows sets the rows to show as data in the table.\nfunc (m Model) WithRows(rows []Row) Model {\n\tm.rows = rows\n\tm.visibleRowCacheUpdated = false\n\n\tif m.rowCursorIndex >= len(m.rows) {\n\t\tm.rowCursorIndex = len(m.rows) - 1\n\t}\n\n\tif m.rowCursorIndex < 0 {\n\t\tm.rowCursorIndex = 0\n\t}\n\n\tif m.pageSize != 0 {\n\t\tmaxPage := m.MaxPages()\n\n\t\t// MaxPages is 1-index, currentPage is 0 index\n\t\tif maxPage <= m.currentPage {\n\t\t\tm.pageLast()\n\t\t}\n\t}\n\n\treturn m\n}\n\n// WithKeyMap sets the key map to use for controls when focused.\nfunc (m Model) WithKeyMap(keyMap KeyMap) Model {\n\tm.keyMap = keyMap\n\n\treturn m\n}\n\n// KeyMap returns a copy of the current key map in use.\nfunc (m Model) KeyMap() KeyMap {\n\treturn m.keyMap\n}\n\n// SelectableRows sets whether or not rows are selectable.  If set, adds a column\n// in the front that acts as a checkbox and responds to controls if Focused.\nfunc (m Model) SelectableRows(selectable bool) Model {\n\tm.selectableRows = selectable\n\n\thasSelectColumn := len(m.columns) > 0 && m.columns[0].key == columnKeySelect\n\n\tif hasSelectColumn != selectable {\n\t\tif selectable {\n\t\t\tm.columns = append([]Column{\n\t\t\t\tNewColumn(columnKeySelect, m.selectedText, len([]rune(m.selectedText))),\n\t\t\t}, m.columns...)\n\t\t} else {\n\t\t\tm.columns = m.columns[1:]\n\t\t}\n\t}\n\n\tm.recalculateWidth()\n\n\treturn m\n}\n\n// HighlightedRow returns the full Row that's currently highlighted by the user.\nfunc (m Model) HighlightedRow() Row {\n\tif len(m.GetVisibleRows()) > 0 {\n\t\treturn m.GetVisibleRows()[m.rowCursorIndex]\n\t}\n\n\t// TODO: Better way to do this without pointers/nil?  Or should it be nil?\n\treturn Row{}\n}\n\n// SelectedRows returns all rows that have been set as selected by the user.\nfunc (m Model) SelectedRows() []Row {\n\tselectedRows := []Row{}\n\n\tfor _, row := range m.GetVisibleRows() {\n\t\tif row.selected {\n\t\t\tselectedRows = append(selectedRows, row)\n\t\t}\n\t}\n\n\treturn selectedRows\n}\n\n// HighlightStyle sets a custom style to use when the row is being highlighted\n// by the cursor.  This should not be used with WithRowStyleFunc.  Instead, use\n// the IsHighlighted field in the style function.\nfunc (m Model) HighlightStyle(style lipgloss.Style) Model {\n\tm.highlightStyle = style\n\n\treturn m\n}\n\n// Focused allows the table to show highlighted rows and take in controls of\n// up/down/space/etc to let the user navigate the table and interact with it.\nfunc (m Model) Focused(focused bool) Model {\n\tm.focused = focused\n\n\treturn m\n}\n\n// Filtered allows the table to show rows that match the filter.\nfunc (m Model) Filtered(filtered bool) Model {\n\tm.filtered = filtered\n\tm.visibleRowCacheUpdated = false\n\n\tif m.minimumHeight > 0 {\n\t\tm.recalculateHeight()\n\t}\n\n\treturn m\n}\n\n// StartFilterTyping focuses the text input to allow user typing to filter.\nfunc (m Model) StartFilterTyping() Model {\n\tm.filterTextInput.Focus()\n\n\treturn m\n}\n\n// WithStaticFooter adds a footer that only displays the given text.\nfunc (m Model) WithStaticFooter(footer string) Model {\n\tm.staticFooter = footer\n\n\tif m.minimumHeight > 0 {\n\t\tm.recalculateHeight()\n\t}\n\n\treturn m\n}\n\n// WithPageSize enables pagination using the given page size.  This can be called\n// again at any point to resize the height of the table.\nfunc (m Model) WithPageSize(pageSize int) Model {\n\tm.pageSize = pageSize\n\n\tmaxPages := m.MaxPages()\n\n\tif m.currentPage >= maxPages {\n\t\tm.currentPage = maxPages - 1\n\t}\n\n\tif m.minimumHeight > 0 {\n\t\tm.recalculateHeight()\n\t}\n\n\treturn m\n}\n\n// WithNoPagination disables pagination in the table.\nfunc (m Model) WithNoPagination() Model {\n\tm.pageSize = 0\n\n\tif m.minimumHeight > 0 {\n\t\tm.recalculateHeight()\n\t}\n\n\treturn m\n}\n\n// WithPaginationWrapping sets whether to wrap around from the beginning to the\n// end when navigating through pages.  Defaults to true.\nfunc (m Model) WithPaginationWrapping(wrapping bool) Model {\n\tm.paginationWrapping = wrapping\n\n\treturn m\n}\n\n// WithSelectedText describes what text to show when selectable rows are enabled.\n// The selectable column header will use the selected text string.\nfunc (m Model) WithSelectedText(unselected, selected string) Model {\n\tm.selectedText = selected\n\tm.unselectedText = unselected\n\n\tif len(m.columns) > 0 && m.columns[0].key == columnKeySelect {\n\t\tm.columns[0] = NewColumn(columnKeySelect, m.selectedText, len([]rune(m.selectedText)))\n\t\tm.recalculateWidth()\n\t}\n\n\treturn m\n}\n\n// WithBaseStyle applies a base style as the default for everything in the table.\n// This is useful for border colors, default alignment, default color, etc.\nfunc (m Model) WithBaseStyle(style lipgloss.Style) Model {\n\tm.baseStyle = style\n\n\treturn m\n}\n\n// WithTargetWidth sets the total target width of the table, including borders.\n// This only takes effect when using flex columns.  When using flex columns,\n// columns will stretch to fill out to the total width given here.\nfunc (m Model) WithTargetWidth(totalWidth int) Model {\n\tm.targetTotalWidth = totalWidth\n\n\tm.recalculateWidth()\n\n\treturn m\n}\n\n// WithMinimumHeight sets the minimum total height of the table, including borders.\nfunc (m Model) WithMinimumHeight(minimumHeight int) Model {\n\tm.minimumHeight = minimumHeight\n\n\tm.recalculateHeight()\n\n\treturn m\n}\n\n// PageDown goes to the next page of a paginated table, wrapping to the first\n// page if the table is already on the last page.\nfunc (m Model) PageDown() Model {\n\tm.pageDown()\n\n\treturn m\n}\n\n// PageUp goes to the previous page of a paginated table, wrapping to the\n// last page if the table is already on the first page.\nfunc (m Model) PageUp() Model {\n\tm.pageUp()\n\n\treturn m\n}\n\n// PageLast goes to the last page of a paginated table.\nfunc (m Model) PageLast() Model {\n\tm.pageLast()\n\n\treturn m\n}\n\n// PageFirst goes to the first page of a paginated table.\nfunc (m Model) PageFirst() Model {\n\tm.pageFirst()\n\n\treturn m\n}\n\n// WithCurrentPage sets the current page (1 as the first page) of a paginated\n// table, bounded to the total number of pages.  The current selected row will\n// be set to the top row of the page if the page changed.\nfunc (m Model) WithCurrentPage(currentPage int) Model {\n\tif m.pageSize == 0 || currentPage == m.CurrentPage() {\n\t\treturn m\n\t}\n\tif currentPage < 1 {\n\t\tcurrentPage = 1\n\t} else {\n\t\tmaxPages := m.MaxPages()\n\n\t\tif currentPage > maxPages {\n\t\t\tcurrentPage = maxPages\n\t\t}\n\t}\n\tm.currentPage = currentPage - 1\n\tm.rowCursorIndex = m.currentPage * m.pageSize\n\n\treturn m\n}\n\n// WithColumns sets the visible columns for the table, so that columns can be\n// added/removed/resized or headers rewritten.\nfunc (m Model) WithColumns(columns []Column) Model {\n\t// Deep copy to avoid edits\n\tm.columns = make([]Column, len(columns))\n\tcopy(m.columns, columns)\n\n\tm.recalculateWidth()\n\n\tif m.selectableRows {\n\t\t// Re-add the selectable column\n\t\tm = m.SelectableRows(true)\n\t}\n\n\treturn m\n}\n\n// WithFilterInput makes the table use the provided text input bubble for\n// filtering rather than using the built-in default.  This allows for external\n// text input controls to be used.\nfunc (m Model) WithFilterInput(input textinput.Model) Model {\n\tif m.filterTextInput.Value() != input.Value() {\n\t\tm.pageFirst()\n\t}\n\n\tm.filterTextInput = input\n\tm.visibleRowCacheUpdated = false\n\n\treturn m\n}\n\n// WithFilterInputValue sets the filter value to the given string, immediately\n// applying it as if the user had typed it in.  Useful for external filter inputs\n// that are not necessarily a text input.\nfunc (m Model) WithFilterInputValue(value string) Model {\n\tif m.filterTextInput.Value() != value {\n\t\tm.pageFirst()\n\t}\n\n\tm.filterTextInput.SetValue(value)\n\tm.filterTextInput.Blur()\n\tm.visibleRowCacheUpdated = false\n\n\treturn m\n}\n\n// WithFilterFunc adds a filter function to the model. If the function returns\n// true, the row will be included in the filtered results. If the function\n// is nil, the function won't be used and instead the default filtering will be applied,\n// if any.\nfunc (m Model) WithFilterFunc(shouldInclude FilterFunc) Model {\n\tm.filterFunc = shouldInclude\n\n\tm.visibleRowCacheUpdated = false\n\n\treturn m\n}\n\n// WithFuzzyFilter enables fuzzy filtering for the table.\nfunc (m Model) WithFuzzyFilter() Model {\n\treturn m.WithFilterFunc(filterFuncFuzzy)\n}\n\n// WithFooterVisibility sets the visibility of the footer.\nfunc (m Model) WithFooterVisibility(visibility bool) Model {\n\tm.footerVisible = visibility\n\n\tif m.minimumHeight > 0 {\n\t\tm.recalculateHeight()\n\t}\n\n\treturn m\n}\n\n// WithHeaderVisibility sets the visibility of the header.\nfunc (m Model) WithHeaderVisibility(visibility bool) Model {\n\tm.headerVisible = visibility\n\n\tif m.minimumHeight > 0 {\n\t\tm.recalculateHeight()\n\t}\n\n\treturn m\n}\n\n// WithMaxTotalWidth sets the maximum total width that the table should render.\n// If this width is exceeded by either the target width or by the total width\n// of all the columns (including borders!), anything extra will be treated as\n// overflow and horizontal scrolling will be enabled to see the rest.\nfunc (m Model) WithMaxTotalWidth(maxTotalWidth int) Model {\n\tm.maxTotalWidth = maxTotalWidth\n\n\tm.recalculateWidth()\n\n\treturn m\n}\n\n// WithHorizontalFreezeColumnCount freezes the given number of columns to the\n// left side.  This is useful for things like ID or Name columns that should\n// always be visible even when scrolling.\nfunc (m Model) WithHorizontalFreezeColumnCount(columnsToFreeze int) Model {\n\tm.horizontalScrollFreezeColumnsCount = columnsToFreeze\n\n\tm.recalculateWidth()\n\n\treturn m\n}\n\n// ScrollRight moves one column to the right.  Use with WithMaxTotalWidth.\nfunc (m Model) ScrollRight() Model {\n\tm.scrollRight()\n\n\treturn m\n}\n\n// ScrollLeft moves one column to the left.  Use with WithMaxTotalWidth.\nfunc (m Model) ScrollLeft() Model {\n\tm.scrollLeft()\n\n\treturn m\n}\n\n// WithMissingDataIndicator sets an indicator to use when data for a column is\n// not found in a given row.  Note that this is for completely missing data,\n// an empty string or other zero value that is explicitly set is not considered\n// to be missing.\nfunc (m Model) WithMissingDataIndicator(str string) Model {\n\tm.missingDataIndicator = str\n\n\treturn m\n}\n\n// WithMissingDataIndicatorStyled sets a styled indicator to use when data for\n// a column is not found in a given row.  Note that this is for completely\n// missing data, an empty string or other zero value that is explicitly set is\n// not considered to be missing.\nfunc (m Model) WithMissingDataIndicatorStyled(styled StyledCell) Model {\n\tm.missingDataIndicator = styled\n\n\treturn m\n}\n\n// WithAllRowsDeselected deselects any rows that are currently selected.\nfunc (m Model) WithAllRowsDeselected() Model {\n\trows := m.GetVisibleRows()\n\n\tfor i, row := range rows {\n\t\tif row.selected {\n\t\t\trows[i] = row.Selected(false)\n\t\t}\n\t}\n\n\tm.rows = rows\n\n\treturn m\n}\n\n// WithMultiline sets whether or not to wrap text in cells to multiple lines.\nfunc (m Model) WithMultiline(multiline bool) Model {\n\tm.multiline = multiline\n\n\treturn m\n}\n\n// WithAdditionalShortHelpKeys enables you to add more keybindings to the 'short help' view.\nfunc (m Model) WithAdditionalShortHelpKeys(keys []key.Binding) Model {\n\tm.additionalShortHelpKeys = func() []key.Binding {\n\t\treturn keys\n\t}\n\n\treturn m\n}\n\n// WithAdditionalFullHelpKeys enables you to add more keybindings to the 'full help' view.\nfunc (m Model) WithAdditionalFullHelpKeys(keys []key.Binding) Model {\n\tm.additionalFullHelpKeys = func() []key.Binding {\n\t\treturn keys\n\t}\n\n\treturn m\n}\n\n// WithGlobalMetadata applies the given metadata to the table. This metadata is passed to\n// some functions in FilterFuncInput and StyleFuncInput to enable more advanced decisions,\n// such as setting some global theme variable to reference, etc. Has no effect otherwise.\nfunc (m Model) WithGlobalMetadata(metadata map[string]any) Model {\n\tm.metadata = metadata\n\n\treturn m\n}\n"
  },
  {
    "path": "table/options_test.go",
    "content": "package table\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestWithHighlightedRowSet(t *testing.T) {\n\thighlightedIndex := 1\n\n\tcols := []Column{\n\t\tNewColumn(\"id\", \"ID\", 3),\n\t}\n\n\tmodel := New(cols).WithRows([]Row{\n\t\tNewRow(RowData{\n\t\t\t\"id\": \"first\",\n\t\t}),\n\t\tNewRow(RowData{\n\t\t\t\"id\": \"second\",\n\t\t}),\n\t}).WithHighlightedRow(highlightedIndex)\n\n\tassert.Equal(t, model.rows[highlightedIndex], model.HighlightedRow())\n}\n\nfunc TestWithHighlightedRowSetNegative(t *testing.T) {\n\thighlightedIndex := -1\n\n\tcols := []Column{\n\t\tNewColumn(\"id\", \"ID\", 3),\n\t}\n\n\tmodel := New(cols).WithRows([]Row{\n\t\tNewRow(RowData{\n\t\t\t\"id\": \"first\",\n\t\t}),\n\t\tNewRow(RowData{\n\t\t\t\"id\": \"second\",\n\t\t}),\n\t}).WithHighlightedRow(highlightedIndex)\n\n\tassert.Equal(t, model.rows[0], model.HighlightedRow())\n}\n\nfunc TestWithHighlightedRowSetTooHigh(t *testing.T) {\n\thighlightedIndex := 2\n\n\tcols := []Column{\n\t\tNewColumn(\"id\", \"ID\", 3),\n\t}\n\n\tmodel := New(cols).WithRows([]Row{\n\t\tNewRow(RowData{\n\t\t\t\"id\": \"first\",\n\t\t}),\n\t\tNewRow(RowData{\n\t\t\t\"id\": \"second\",\n\t\t}),\n\t}).WithHighlightedRow(highlightedIndex)\n\n\tassert.Equal(t, model.rows[1], model.HighlightedRow())\n}\n\n// This is long only because it's a lot of repetitive test cases\n//\n//nolint:funlen\nfunc TestPageOptions(t *testing.T) {\n\tconst (\n\t\tpageSize = 5\n\t\trowCount = 30\n\t)\n\n\tcols := []Column{\n\t\tNewColumn(\"id\", \"ID\", 3),\n\t}\n\n\trows := make([]Row, rowCount)\n\n\tmodel := New(cols).WithRows(rows).WithPageSize(pageSize)\n\tassert.Equal(t, 1, model.CurrentPage())\n\n\tmodel = model.PageDown()\n\tassert.Equal(t, 2, model.CurrentPage())\n\n\tmodel = model.PageDown()\n\tmodel = model.PageUp()\n\tassert.Equal(t, 2, model.CurrentPage())\n\n\tmodel = model.PageLast()\n\tassert.Equal(t, 6, model.CurrentPage())\n\n\tmodel = model.PageLast()\n\tmodel = model.PageLast()\n\tassert.Equal(t, 6, model.CurrentPage())\n\n\tmodel = model.PageFirst()\n\tassert.Equal(t, 1, model.CurrentPage())\n\n\tmodel = model.PageFirst()\n\tmodel = model.PageFirst()\n\tassert.Equal(t, 1, model.CurrentPage())\n\n\tmodel = model.PageUp()\n\tassert.Equal(t, 6, model.CurrentPage())\n\n\tmodel = model.PageDown()\n\tassert.Equal(t, 1, model.CurrentPage())\n\n\tmodel = model.WithCurrentPage(3)\n\tmodel = model.WithCurrentPage(3)\n\tmodel = model.WithCurrentPage(3)\n\tassert.Equal(t, 3, model.CurrentPage())\n\tassert.Equal(t, 10, model.rowCursorIndex)\n\n\tmodel = model.WithCurrentPage(-1)\n\tassert.Equal(t, 1, model.CurrentPage())\n\tassert.Equal(t, 0, model.rowCursorIndex)\n\n\tmodel = model.WithCurrentPage(0)\n\tassert.Equal(t, 1, model.CurrentPage())\n\tassert.Equal(t, 0, model.rowCursorIndex)\n\n\tmodel = model.WithCurrentPage(7)\n\tassert.Equal(t, 6, model.CurrentPage())\n\tassert.Equal(t, 25, model.rowCursorIndex)\n\n\tmodel.rowCursorIndex = 26\n\tmodel = model.WithCurrentPage(6)\n\tassert.Equal(t, 6, model.CurrentPage())\n\tassert.Equal(t, 26, model.rowCursorIndex)\n\n\tmodel = model.WithFooterVisibility(false)\n\tassert.Equal(t, \"\", model.renderFooter(10, false))\n\n\tmodel = model.WithFooterVisibility(true)\n\tassert.Greater(t, len(model.renderFooter(10, false)), 10)\n\tassert.Contains(t, model.renderFooter(10, false), \"6/6\")\n}\n\nfunc TestMinimumHeightOptions(t *testing.T) {\n\tcolumns := []Column{\n\t\tNewColumn(\"ka\", \"a\", 3),\n\t\tNewColumn(\"kb\", \"b\", 4),\n\t\tNewColumn(\"kc\", \"c\", 5),\n\t}\n\n\tmodel := New(columns).WithMinimumHeight(10)\n\tassert.Equal(t, 10, model.minimumHeight)\n\tassert.Equal(t, 3, model.metaHeight)\n\n\tmodel = model.WithPageSize(2)\n\tassert.Equal(t, 5, model.metaHeight)\n\n\tmodel = model.WithNoPagination()\n\tassert.Equal(t, 3, model.metaHeight)\n\n\tmodel = model.WithStaticFooter(\"footer with\\nmultiple lines\")\n\tassert.Equal(t, 6, model.metaHeight)\n\n\tmodel = model.WithStaticFooter(\"\").Filtered(true)\n\tassert.Equal(t, 5, model.metaHeight)\n\n\tmodel = model.WithFooterVisibility(false)\n\tassert.Equal(t, 3, model.metaHeight)\n\n\tmodel = model.WithHeaderVisibility(false)\n\tassert.Equal(t, 1, model.metaHeight)\n}\n\n// This is long only because the test cases are larger\n//\n//nolint:funlen\nfunc TestSelectRowsProgramatically(t *testing.T) {\n\tconst col = \"id\"\n\n\ttests := map[string]struct {\n\t\trows        []Row\n\t\tselectedIDs []int\n\t}{\n\t\t\"no rows selected\": {\n\t\t\t[]Row{\n\t\t\t\tNewRow(RowData{col: 1}),\n\t\t\t\tNewRow(RowData{col: 2}),\n\t\t\t\tNewRow(RowData{col: 3}),\n\t\t\t},\n\t\t\t[]int{},\n\t\t},\n\n\t\t\"all rows selected\": {\n\t\t\t[]Row{\n\t\t\t\tNewRow(RowData{col: 1}).Selected(true),\n\t\t\t\tNewRow(RowData{col: 2}).Selected(true),\n\t\t\t\tNewRow(RowData{col: 3}).Selected(true),\n\t\t\t},\n\t\t\t[]int{1, 2, 3},\n\t\t},\n\n\t\t\"first row selected\": {\n\t\t\t[]Row{\n\t\t\t\tNewRow(RowData{col: 1}).Selected(true),\n\t\t\t\tNewRow(RowData{col: 2}),\n\t\t\t\tNewRow(RowData{col: 3}),\n\t\t\t},\n\t\t\t[]int{1},\n\t\t},\n\n\t\t\"last row selected\": {\n\t\t\t[]Row{\n\t\t\t\tNewRow(RowData{col: 1}),\n\t\t\t\tNewRow(RowData{col: 2}),\n\t\t\t\tNewRow(RowData{col: 3}).Selected(true),\n\t\t\t},\n\t\t\t[]int{3},\n\t\t},\n\t}\n\n\tbaseModel := New([]Column{\n\t\tNewColumn(col, col, 1),\n\t})\n\n\tfor name, test := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tmodel := baseModel.WithRows(test.rows)\n\t\t\tsel := model.SelectedRows()\n\n\t\t\tassert.Equal(t, len(test.selectedIDs), len(sel))\n\t\t\tfor i, id := range test.selectedIDs {\n\t\t\t\tassert.Equal(t, id, sel[i].Data[col], \"expecting row %d to have same %s column value\", i)\n\t\t\t}\n\n\t\t\tmodel = model.WithAllRowsDeselected()\n\t\t\tassert.Len(t, model.SelectedRows(), 0, \"Did not deselect all rows\")\n\t\t})\n\t}\n}\n\nfunc TestDefaultBorderIsDefault(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"id\", \"ID\", 1),\n\t}).WithRows([]Row{\n\t\tNewRow(RowData{\"id\": 1}),\n\t\tNewRow(RowData{\"id\": 2}),\n\t\tNewRow(RowData{\"id\": 3}),\n\t})\n\n\trenderedInitial := model.View()\n\n\tmodel = model.BorderRounded()\n\trenderedRounded := model.View()\n\n\tmodel = model.BorderDefault()\n\trenderedDefault := model.View()\n\n\tassert.NotEqual(t, renderedInitial, renderedRounded, \"Rounded border should differ from default\")\n\tassert.Equal(t, renderedInitial, renderedDefault, \"Default border should match initial state\")\n}\n\nfunc BenchmarkSelectedRows(b *testing.B) {\n\tconst N = 1000\n\n\tb.ReportAllocs()\n\n\trows := make([]Row, 0, N)\n\tfor i := 0; i < N; i++ {\n\t\trows = append(rows, NewRow(RowData{\"row\": i}).Selected(i%2 == 0))\n\t}\n\n\tmodel := New([]Column{\n\t\tNewColumn(\"row\", \"Row\", 4),\n\t}).WithRows(rows)\n\n\tvar sel []Row\n\n\tb.ResetTimer()\n\n\tfor i := 0; i < b.N; i++ {\n\t\tsel = model.SelectedRows()\n\t}\n\n\tRows = sel\n}\n\nvar Rows []Row\n"
  },
  {
    "path": "table/overflow.go",
    "content": "package table\n\nimport \"github.com/charmbracelet/lipgloss\"\n\nconst columnKeyOverflowRight = \"___overflow_r___\"\nconst columnKeyOverflowLeft = \"___overflow_l__\"\n\nfunc genOverflowStyle(base lipgloss.Style, width int) lipgloss.Style {\n\treturn base.Width(width).Align(lipgloss.Right)\n}\n\nfunc genOverflowColumnRight(width int) Column {\n\treturn NewColumn(columnKeyOverflowRight, \">\", width)\n}\n\nfunc genOverflowColumnLeft(width int) Column {\n\treturn NewColumn(columnKeyOverflowLeft, \"<\", width)\n}\n"
  },
  {
    "path": "table/pagination.go",
    "content": "package table\n\n// PageSize returns the current page size for the table, or 0 if there is no\n// pagination enabled.\nfunc (m *Model) PageSize() int {\n\treturn m.pageSize\n}\n\n// CurrentPage returns the current page that the table is on, starting from an\n// index of 1.\nfunc (m *Model) CurrentPage() int {\n\treturn m.currentPage + 1\n}\n\n// MaxPages returns the maximum number of pages that are visible.\nfunc (m *Model) MaxPages() int {\n\ttotalRows := len(m.GetVisibleRows())\n\n\tif m.pageSize == 0 || totalRows == 0 {\n\t\treturn 1\n\t}\n\n\treturn (totalRows-1)/m.pageSize + 1\n}\n\n// TotalRows returns the current total row count of the table.  If the table is\n// paginated, this is the total number of rows across all pages.\nfunc (m *Model) TotalRows() int {\n\treturn len(m.GetVisibleRows())\n}\n\n// VisibleIndices returns the current visible rows by their 0 based index.\n// Useful for custom pagination footers.\nfunc (m *Model) VisibleIndices() (start, end int) {\n\ttotalRows := len(m.GetVisibleRows())\n\n\tif m.pageSize == 0 {\n\t\tstart = 0\n\t\tend = totalRows - 1\n\n\t\treturn start, end\n\t}\n\n\tstart = m.pageSize * m.currentPage\n\tend = start + m.pageSize - 1\n\n\tif end >= totalRows {\n\t\tend = totalRows - 1\n\t}\n\n\treturn start, end\n}\n\nfunc (m *Model) pageDown() {\n\tif m.pageSize == 0 || len(m.GetVisibleRows()) <= m.pageSize {\n\t\treturn\n\t}\n\n\tm.currentPage++\n\n\tmaxPageIndex := m.MaxPages() - 1\n\n\tif m.currentPage > maxPageIndex {\n\t\tif m.paginationWrapping {\n\t\t\tm.currentPage = 0\n\t\t} else {\n\t\t\tm.currentPage = maxPageIndex\n\t\t}\n\t}\n\n\tm.rowCursorIndex = m.currentPage * m.pageSize\n}\n\nfunc (m *Model) pageUp() {\n\tif m.pageSize == 0 || len(m.GetVisibleRows()) <= m.pageSize {\n\t\treturn\n\t}\n\n\tm.currentPage--\n\n\tmaxPageIndex := m.MaxPages() - 1\n\n\tif m.currentPage < 0 {\n\t\tif m.paginationWrapping {\n\t\t\tm.currentPage = maxPageIndex\n\t\t} else {\n\t\t\tm.currentPage = 0\n\t\t}\n\t}\n\n\tm.rowCursorIndex = m.currentPage * m.pageSize\n}\n\nfunc (m *Model) pageFirst() {\n\tm.currentPage = 0\n\tm.rowCursorIndex = 0\n}\n\nfunc (m *Model) pageLast() {\n\tm.currentPage = m.MaxPages() - 1\n\tm.rowCursorIndex = m.currentPage * m.pageSize\n}\n\nfunc (m *Model) expectedPageForRowIndex(rowIndex int) int {\n\tif m.pageSize == 0 {\n\t\treturn 0\n\t}\n\n\texpectedPage := rowIndex / m.pageSize\n\n\treturn expectedPage\n}\n"
  },
  {
    "path": "table/pagination_test.go",
    "content": "package table\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc genPaginationTable(count, pageSize int) Model {\n\tmodel := New([]Column{\n\t\tNewColumn(\"id\", \"ID\", 3),\n\t})\n\n\trows := []Row{}\n\n\tfor i := 1; i <= count; i++ {\n\t\trows = append(rows, NewRow(RowData{\n\t\t\t\"id\": i,\n\t\t}))\n\t}\n\n\treturn model.WithRows(rows).WithPageSize(pageSize)\n}\n\nfunc paginationRowID(row Row) int {\n\trowID, ok := row.Data[\"id\"].(int)\n\n\tif !ok {\n\t\tpanic(\"id not int, bad test\")\n\t}\n\n\treturn rowID\n}\n\nfunc getVisibleRows(m *Model) []Row {\n\tstart, end := m.VisibleIndices()\n\n\treturn m.GetVisibleRows()[start : end+1]\n}\n\nfunc TestPaginationAccessors(t *testing.T) {\n\tconst (\n\t\tnumRows  = 100\n\t\tpageSize = 20\n\t)\n\n\tmodel := genPaginationTable(numRows, pageSize)\n\n\tassert.Equal(t, numRows, model.TotalRows())\n\tassert.Equal(t, pageSize, model.PageSize())\n}\n\nfunc TestPaginationNoPageSizeReturnsAll(t *testing.T) {\n\tconst (\n\t\tnumRows  = 100\n\t\tpageSize = 0\n\t)\n\n\tmodel := genPaginationTable(numRows, pageSize)\n\n\tpaginatedRows := getVisibleRows(&model)\n\n\tassert.Len(t, paginatedRows, numRows)\n\tassert.Equal(t, 1, model.MaxPages())\n}\n\nfunc TestPaginationEmptyTableReturnsNoRows(t *testing.T) {\n\tconst (\n\t\tnumRows  = 0\n\t\tpageSize = 10\n\t)\n\n\tmodel := genPaginationTable(numRows, pageSize)\n\n\tpaginatedRows := getVisibleRows(&model)\n\n\tassert.Len(t, paginatedRows, numRows)\n}\n\nfunc TestPaginationDefaultsToAllRows(t *testing.T) {\n\tconst numRows = 100\n\n\tmodel := genPaginationTable(numRows, 0)\n\n\tpaginatedRows := getVisibleRows(&model)\n\n\tassert.Len(t, paginatedRows, numRows)\n}\n\nfunc TestPaginationReturnsPartialFirstPage(t *testing.T) {\n\tconst (\n\t\tnumRows  = 10\n\t\tpageSize = 20\n\t)\n\n\tmodel := genPaginationTable(numRows, pageSize)\n\n\tpaginatedRows := getVisibleRows(&model)\n\n\tassert.Len(t, paginatedRows, numRows)\n}\n\nfunc TestPaginationReturnsFirstFullPage(t *testing.T) {\n\tconst (\n\t\tpageSize = 10\n\t\tnumRows  = 20\n\t)\n\n\tmodel := genPaginationTable(numRows, pageSize)\n\n\tpaginatedRows := getVisibleRows(&model)\n\n\tassert.Len(t, paginatedRows, pageSize)\n\n\tfor i, row := range paginatedRows {\n\t\tassert.Equal(t, i+1, paginationRowID(row))\n\t}\n}\n\nfunc TestPaginationReturnsSecondFullPageAfterMoving(t *testing.T) {\n\tconst (\n\t\tpageSize = 10\n\t\tnumRows  = 30\n\t)\n\n\tmodel := genPaginationTable(numRows, pageSize)\n\n\tmodel.pageDown()\n\n\tpaginatedRows := getVisibleRows(&model)\n\n\tassert.Len(t, paginatedRows, pageSize)\n\n\tfor i, row := range paginatedRows {\n\t\tassert.Equal(t, i+11, paginationRowID(row))\n\t}\n}\n\nfunc TestPaginationReturnsPartialFinalPage(t *testing.T) {\n\tconst (\n\t\tpageSize = 10\n\t\tnumRows  = 15\n\t)\n\n\tmodel := genPaginationTable(numRows, pageSize)\n\n\tmodel.pageDown()\n\n\tpaginatedRows := getVisibleRows(&model)\n\n\tassert.Len(t, paginatedRows, numRows-pageSize)\n\n\tfor i, row := range paginatedRows {\n\t\tassert.Equal(t, i+11, paginationRowID(row))\n\t}\n}\n\nfunc TestPaginationWrapsUpPartial(t *testing.T) {\n\tconst (\n\t\tpageSize = 10\n\t\tnumRows  = 15\n\t)\n\n\tmodel := genPaginationTable(numRows, pageSize)\n\n\tmodel.pageUp()\n\n\tpaginatedRows := getVisibleRows(&model)\n\n\tassert.Len(t, paginatedRows, numRows-pageSize)\n\n\tfor i, row := range paginatedRows {\n\t\tassert.Equal(t, i+11, paginationRowID(row))\n\t}\n}\n\nfunc TestPaginationWrapsUpFull(t *testing.T) {\n\tconst (\n\t\tpageSize = 10\n\t\tnumRows  = 20\n\t)\n\n\tmodel := genPaginationTable(numRows, pageSize)\n\n\tmodel.pageUp()\n\n\tpaginatedRows := getVisibleRows(&model)\n\n\tassert.Len(t, paginatedRows, numRows-pageSize)\n\n\tfor i, row := range paginatedRows {\n\t\tassert.Equal(t, i+11, paginationRowID(row))\n\t}\n}\n\nfunc TestPaginationWrapsUpSelf(t *testing.T) {\n\tconst (\n\t\tpageSize = 10\n\t\tnumRows  = 10\n\t)\n\n\tmodel := genPaginationTable(numRows, pageSize)\n\n\tmodel.pageUp()\n\n\tpaginatedRows := getVisibleRows(&model)\n\n\tassert.Len(t, paginatedRows, numRows)\n\n\tfor i, row := range paginatedRows {\n\t\tassert.Equal(t, i+1, paginationRowID(row))\n\t}\n}\n\nfunc TestPaginationWrapsDown(t *testing.T) {\n\tconst (\n\t\tpageSize = 10\n\t\tnumRows  = 15\n\t)\n\n\tmodel := genPaginationTable(numRows, pageSize)\n\n\tmodel.pageDown()\n\tmodel.pageDown()\n\n\tpaginatedRows := getVisibleRows(&model)\n\n\tassert.Len(t, paginatedRows, pageSize)\n\n\tfor i, row := range paginatedRows {\n\t\tassert.Equal(t, i+1, paginationRowID(row))\n\t}\n}\n\nfunc TestPaginationWrapsDownSelf(t *testing.T) {\n\tconst (\n\t\tpageSize = 10\n\t\tnumRows  = 10\n\t)\n\n\tmodel := genPaginationTable(numRows, pageSize)\n\n\tmodel.pageDown()\n\tmodel.pageDown()\n\n\tpaginatedRows := getVisibleRows(&model)\n\n\tassert.Len(t, paginatedRows, pageSize)\n\n\tfor i, row := range paginatedRows {\n\t\tassert.Equal(t, i+1, paginationRowID(row))\n\t}\n}\n\nfunc TestPaginationHighlightFirstOnPageDown(t *testing.T) {\n\tconst (\n\t\tpageSize = 10\n\t\tnumRows  = 20\n\t)\n\n\tmodel := genPaginationTable(numRows, pageSize)\n\n\tassert.Equal(t, 1, paginationRowID(model.HighlightedRow()), \"Initial test setup wrong, test code may be bad\")\n\n\tmodel.pageDown()\n\n\tassert.Equal(t, 11, paginationRowID(model.HighlightedRow()), \"Did not highlight expected row\")\n}\n\n// This is long because of various test cases, not because of logic\n//\n//nolint:funlen\nfunc TestExpectedPageForRowIndex(t *testing.T) {\n\ttests := []struct {\n\t\tname         string\n\t\ttotalRows    int\n\t\tpageSize     int\n\t\trowIndex     int\n\t\texpectedPage int\n\t}{\n\t\t{\n\t\t\tname: \"Empty\",\n\t\t},\n\t\t{\n\t\t\tname:         \"No pages\",\n\t\t\ttotalRows:    50,\n\t\t\tpageSize:     0,\n\t\t\trowIndex:     37,\n\t\t\texpectedPage: 0,\n\t\t},\n\t\t{\n\t\t\tname:         \"One page\",\n\t\t\ttotalRows:    50,\n\t\t\tpageSize:     50,\n\t\t\trowIndex:     37,\n\t\t\texpectedPage: 0,\n\t\t},\n\t\t{\n\t\t\tname:         \"First page\",\n\t\t\ttotalRows:    50,\n\t\t\tpageSize:     30,\n\t\t\trowIndex:     17,\n\t\t\texpectedPage: 0,\n\t\t},\n\t\t{\n\t\t\tname:         \"Second page\",\n\t\t\ttotalRows:    50,\n\t\t\tpageSize:     30,\n\t\t\trowIndex:     37,\n\t\t\texpectedPage: 1,\n\t\t},\n\t\t{\n\t\t\tname:         \"First page first row\",\n\t\t\ttotalRows:    50,\n\t\t\tpageSize:     30,\n\t\t\trowIndex:     0,\n\t\t\texpectedPage: 0,\n\t\t},\n\t\t{\n\t\t\tname:         \"First page last row\",\n\t\t\ttotalRows:    50,\n\t\t\tpageSize:     30,\n\t\t\trowIndex:     29,\n\t\t\texpectedPage: 0,\n\t\t},\n\t\t{\n\t\t\tname:         \"Second page first row\",\n\t\t\ttotalRows:    50,\n\t\t\tpageSize:     30,\n\t\t\trowIndex:     30,\n\t\t\texpectedPage: 1,\n\t\t},\n\t\t{\n\t\t\tname:         \"Second page last row\",\n\t\t\ttotalRows:    50,\n\t\t\tpageSize:     30,\n\t\t\trowIndex:     49,\n\t\t\texpectedPage: 1,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tmodel := genPaginationTable(test.totalRows, test.pageSize)\n\n\t\t\tpage := model.expectedPageForRowIndex(test.rowIndex)\n\n\t\t\tassert.Equal(t, test.expectedPage, page)\n\t\t})\n\t}\n}\n\nfunc TestClearPagination(t *testing.T) {\n\tconst (\n\t\tpageSize = 10\n\t\tnumRows  = 20\n\t)\n\n\tmodel := genPaginationTable(numRows, pageSize)\n\n\tassert.Equal(t, 1, model.expectedPageForRowIndex(11))\n\n\tmodel = model.WithNoPagination()\n\n\tassert.Equal(t, 0, model.expectedPageForRowIndex(11))\n}\n\nfunc TestPaginationSetsLastPageWithFewerRows(t *testing.T) {\n\tconst (\n\t\tpageSize        = 10\n\t\tnumRowsOriginal = 30\n\t\tnumRowsAfter    = 18\n\t)\n\n\tmodel := genPaginationTable(numRowsOriginal, pageSize)\n\tmodel.pageUp()\n\n\tassert.Equal(t, 3, model.CurrentPage())\n\n\trows := []Row{}\n\n\tfor i := 1; i <= numRowsAfter; i++ {\n\t\trows = append(rows, NewRow(RowData{\n\t\t\t\"id\": i,\n\t\t}))\n\t}\n\n\tmodel = model.WithRows(rows)\n\n\tassert.Equal(t, 2, model.CurrentPage())\n}\n\nfunc TestPaginationBoundsToMaxPageOnResize(t *testing.T) {\n\tconst (\n\t\tpageSize = 5\n\t\tnumRows  = 20\n\t)\n\n\tmodel := genPaginationTable(numRows, pageSize)\n\n\tassert.Equal(t, model.CurrentPage(), 1)\n\n\tmodel.pageUp()\n\n\tassert.Equal(t, model.CurrentPage(), 4)\n\n\tmodel = model.WithPageSize(10)\n\n\tassert.Equal(t, model.CurrentPage(), 2)\n\tassert.Equal(t, model.MaxPages(), 2)\n}\n"
  },
  {
    "path": "table/query.go",
    "content": "package table\n\n// GetColumnSorting returns the current sorting rules for the table as a list of\n// SortColumns, which are applied from first to last.  This means that data will\n// be grouped by the later elements in the list.  The returned list is a copy\n// and modifications will have no effect.\nfunc (m *Model) GetColumnSorting() []SortColumn {\n\tc := make([]SortColumn, len(m.sortOrder))\n\n\tcopy(c, m.sortOrder)\n\n\treturn c\n}\n\n// GetCanFilter returns true if the table enables filtering at all.  This does\n// not say whether a filter is currently active, only that the feature is enabled.\nfunc (m *Model) GetCanFilter() bool {\n\treturn m.filtered\n}\n\n// GetIsFilterActive returns true if the table is currently being filtered.  This\n// does not say whether the table CAN be filtered, only whether or not a filter\n// is actually currently being applied.\nfunc (m *Model) GetIsFilterActive() bool {\n\treturn m.filterTextInput.Value() != \"\"\n}\n\n// GetIsFilterInputFocused returns true if the table's built-in filter input is\n// currently focused.\nfunc (m *Model) GetIsFilterInputFocused() bool {\n\treturn m.filterTextInput.Focused()\n}\n\n// GetCurrentFilter returns the current filter text being applied, or an empty\n// string if none is applied.\nfunc (m *Model) GetCurrentFilter() string {\n\treturn m.filterTextInput.Value()\n}\n\n// GetVisibleRows returns sorted and filtered rows.\nfunc (m *Model) GetVisibleRows() []Row {\n\tif m.visibleRowCacheUpdated {\n\t\treturn m.visibleRowCache\n\t}\n\n\trows := make([]Row, len(m.rows))\n\tcopy(rows, m.rows)\n\tif m.filtered {\n\t\trows = m.getFilteredRows(rows)\n\t}\n\trows = getSortedRows(m.sortOrder, rows)\n\n\tm.visibleRowCache = rows\n\tm.visibleRowCacheUpdated = true\n\n\treturn rows\n}\n\n// GetHighlightedRowIndex returns the index of the Row that's currently highlighted\n// by the user.\nfunc (m *Model) GetHighlightedRowIndex() int {\n\treturn m.rowCursorIndex\n}\n\n// GetFocused returns whether or not the table is focused and is receiving inputs.\nfunc (m *Model) GetFocused() bool {\n\treturn m.focused\n}\n\n// GetHorizontalScrollColumnOffset returns how many columns to the right the table\n// has been scrolled.  0 means the table is all the way to the left, which is\n// the starting default.\nfunc (m *Model) GetHorizontalScrollColumnOffset() int {\n\treturn m.horizontalScrollOffsetCol\n}\n\n// GetHeaderVisibility returns true if the header has been set to visible (default)\n// or false if the header has been set to hidden.\nfunc (m *Model) GetHeaderVisibility() bool {\n\treturn m.headerVisible\n}\n\n// GetFooterVisibility returns true if the footer has been set to\n// visible (default) or false if the footer has been set to hidden.\n// Note that even if the footer is visible it will only be rendered if\n// it has contents.\nfunc (m *Model) GetFooterVisibility() bool {\n\treturn m.footerVisible\n}\n\n// GetPaginationWrapping returns true if pagination wrapping is enabled, or false\n// if disabled.  If disabled, navigating through pages will stop at the first\n// and last pages.\nfunc (m *Model) GetPaginationWrapping() bool {\n\treturn m.paginationWrapping\n}\n"
  },
  {
    "path": "table/query_test.go",
    "content": "package table\n\nimport (\n\t\"testing\"\n\n\t\"github.com/charmbracelet/bubbles/textinput\"\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestGetColumnSorting(t *testing.T) {\n\tcols := []Column{\n\t\tNewColumn(\"a\", \"a\", 3),\n\t\tNewColumn(\"b\", \"b\", 3),\n\t\tNewColumn(\"c\", \"c\", 3),\n\t}\n\n\tmodel := New(cols).SortByAsc(\"b\")\n\n\tsorted := model.GetColumnSorting()\n\n\tassert.Len(t, sorted, 1, \"Should only have one column\")\n\tassert.Equal(t, sorted[0].ColumnKey, \"b\", \"Should sort column b\")\n\tassert.Equal(t, sorted[0].Direction, SortDirectionAsc, \"Should be ascending\")\n\n\tsorted[0].Direction = SortDirectionDesc\n\n\tassert.NotEqual(\n\t\tt,\n\t\tmodel.sortOrder[0].Direction,\n\t\tsorted[0].Direction,\n\t\t\"Should not have been able to modify actual values\",\n\t)\n}\n\nfunc TestGetFilterData(t *testing.T) {\n\tmodel := New([]Column{})\n\n\tassert.False(t, model.GetIsFilterActive(), \"Should not start with filter active\")\n\tassert.False(t, model.GetCanFilter(), \"Should not start with filter ability\")\n\tassert.Equal(t, model.GetCurrentFilter(), \"\", \"Filter string should be empty\")\n\n\tmodel = model.Filtered(true)\n\n\tassert.False(t, model.GetIsFilterActive(), \"Should not be filtered just because the ability was activated\")\n\tassert.True(t, model.GetCanFilter(), \"Filter feature should be enabled\")\n\tassert.Equal(t, model.GetCurrentFilter(), \"\", \"Filter string should be empty\")\n\n\tmodel.filterTextInput.SetValue(\"a\")\n\n\tassert.True(t, model.GetIsFilterActive(), \"Typing anything into box should mark as filtered\")\n\tassert.True(t, model.GetCanFilter(), \"Filter feature should be enabled\")\n\tassert.Equal(t, model.GetCurrentFilter(), \"a\", \"Filter string should be what was typed\")\n}\n\nfunc TestGetVisibleRows(t *testing.T) {\n\tinput := textinput.Model{}\n\tinput.SetValue(\"AAA\")\n\tcolumns := []Column{NewColumn(\"title\", \"title\", 10).WithFiltered(true)}\n\trows := []Row{\n\t\tNewRow(RowData{\n\t\t\t\"title\":       \"AAA\",\n\t\t\t\"description\": \"\",\n\t\t}),\n\t\tNewRow(RowData{\n\t\t\t\"title\":       \"BBB\",\n\t\t\t\"description\": \"\",\n\t\t}),\n\t\tNewRow(RowData{\n\t\t\t\"title\":       \"CCC\",\n\t\t\t\"description\": \"\",\n\t\t}),\n\t}\n\tm := Model{filtered: true, filterTextInput: input, columns: columns, rows: rows}\n\tvisibleRows := m.GetVisibleRows()\n\tassert.Len(t, visibleRows, 1)\n}\n\nfunc TestGetHighlightedRowIndex(t *testing.T) {\n\tmodel := New([]Column{})\n\n\tassert.Equal(t, 0, model.GetHighlightedRowIndex(), \"Empty table should still safely have 0 index highlighted\")\n\n\t// We don't actually need data to test this\n\tempty := RowData{}\n\tmodel = model.WithRows([]Row{NewRow(empty), NewRow(empty)})\n\n\tassert.Equal(t, 0, model.GetHighlightedRowIndex(), \"Unfocused table should start with 0 index\")\n\n\tmodel = model.WithHighlightedRow(1)\n\n\tassert.Equal(t, 1, model.GetHighlightedRowIndex(), \"Table with set highlighted row should return same highlighted row\")\n}\n\nfunc TestGetFocused(t *testing.T) {\n\tmodel := New([]Column{})\n\n\tassert.Equal(t, false, model.GetFocused(), \"Table should not be focused by default\")\n\n\tmodel = model.Focused(true)\n\n\tassert.Equal(t, true, model.GetFocused(), \"Table should be focused after being set\")\n}\n\nfunc TestGetHorizontalScrollColumnOffset(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"1\", \"1\", 4),\n\t\tNewColumn(\"2\", \"2\", 4),\n\t\tNewColumn(\"3\", \"3\", 4),\n\t\tNewColumn(\"4\", \"4\", 4),\n\t}).\n\t\tWithRows([]Row{\n\t\t\tNewRow(RowData{\n\t\t\t\t\"1\": \"x1\",\n\t\t\t\t\"2\": \"x2\",\n\t\t\t\t\"3\": \"x3\",\n\t\t\t\t\"4\": \"x4\",\n\t\t\t}),\n\t\t}).\n\t\tWithMaxTotalWidth(18).\n\t\tFocused(true)\n\n\thitScrollRight := func() {\n\t\tmodel, _ = model.Update(tea.KeyMsg{Type: tea.KeyShiftRight})\n\t}\n\n\thitScrollLeft := func() {\n\t\tmodel, _ = model.Update(tea.KeyMsg{Type: tea.KeyShiftLeft})\n\t}\n\n\tassert.Equal(\n\t\tt,\n\t\t0,\n\t\tmodel.GetHorizontalScrollColumnOffset(),\n\t\t\"Should start to left\",\n\t)\n\n\thitScrollRight()\n\n\tassert.Equal(\n\t\tt,\n\t\t1,\n\t\tmodel.GetHorizontalScrollColumnOffset(),\n\t\t\"Should be 1 after scrolling to the right once\",\n\t)\n\n\thitScrollLeft()\n\tassert.Equal(\n\t\tt,\n\t\t0,\n\t\tmodel.GetHorizontalScrollColumnOffset(),\n\t\t\"Should be back to 0 after moving to the left\",\n\t)\n\n\thitScrollLeft()\n\tassert.Equal(\n\t\tt,\n\t\t0,\n\t\tmodel.GetHorizontalScrollColumnOffset(),\n\t\t\"Should still be 0 after trying to go left again\",\n\t)\n}\n\nfunc TestGetHeaderVisibility(t *testing.T) {\n\tmodel := New([]Column{})\n\n\tassert.True(t, model.GetHeaderVisibility(), \"Header should be visible by default\")\n\n\tmodel = model.WithHeaderVisibility(false)\n\n\tassert.False(t, model.GetHeaderVisibility(), \"Header was not set to hidden\")\n}\n\nfunc TestGetFooterVisibility(t *testing.T) {\n\tmodel := New([]Column{})\n\n\tassert.True(t, model.GetFooterVisibility(), \"Footer should be visible by default\")\n\n\tmodel = model.WithFooterVisibility(false)\n\n\tassert.False(t, model.GetFooterVisibility(), \"Footer was not set to hidden\")\n}\n\nfunc TestGetPaginationWrapping(t *testing.T) {\n\tmodel := New([]Column{})\n\n\tassert.True(t, model.GetPaginationWrapping(), \"Pagination wrapping should default to true\")\n\n\tmodel = model.WithPaginationWrapping(false)\n\n\tassert.False(t, model.GetPaginationWrapping(), \"Pagination wrapping setting did not update after setting option\")\n}\n\nfunc TestGetIsFilterInputFocused(t *testing.T) {\n\tmodel := New([]Column{}).Filtered(true).Focused(true)\n\n\tassert.False(t, model.GetIsFilterInputFocused(), \"Text input shouldn't start focused\")\n\n\tmodel, _ = model.Update(tea.KeyMsg{\n\t\tType:  tea.KeyRunes,\n\t\tRunes: []rune{'/'},\n\t})\n\n\tassert.True(t, model.GetIsFilterInputFocused(), \"Did not trigger text input\")\n\n\tmodel, _ = model.Update(tea.KeyMsg{\n\t\tType: tea.KeyEnter,\n\t})\n\n\tassert.False(t, model.GetIsFilterInputFocused(), \"Should no longer be focused after hitting enter\")\n}\n"
  },
  {
    "path": "table/row.go",
    "content": "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\"\n)\n\n// RowData is a map of string column keys to arbitrary data.  Data with a key\n// that matches a column key will be displayed.  Data with a key that does not\n// match a column key will not be displayed, but will remain attached to the Row.\n// This can be useful for attaching hidden metadata for future reference when\n// retrieving rows.\ntype RowData map[string]any\n\n// Row represents a row in the table with some data keyed to the table columns>\n// Can have a style applied to it such as color/bold.  Create using NewRow().\ntype Row struct {\n\tStyle lipgloss.Style\n\tData  RowData\n\n\tselected bool\n\n\t// id is an internal unique ID to match rows after they're copied\n\tid uint32\n}\n\nvar lastRowID uint32 = 1\n\n// NewRow creates a new row and copies the given row data.\nfunc NewRow(data RowData) Row {\n\trow := Row{\n\t\tData: make(map[string]any),\n\t\tid:   lastRowID,\n\t}\n\n\tatomic.AddUint32(&lastRowID, 1)\n\n\tfor key, val := range data {\n\t\t// Doesn't deep copy val, but close enough for now...\n\t\trow.Data[key] = val\n\t}\n\n\treturn row\n}\n\n// WithStyle uses the given style for the text in the row.\nfunc (r Row) WithStyle(style lipgloss.Style) Row {\n\tr.Style = style.Copy()\n\n\treturn r\n}\n\n//nolint:cyclop,funlen // Breaking this up will be more complicated than it's worth for now\nfunc (m Model) renderRowColumnData(row Row, column Column, rowStyle lipgloss.Style, borderStyle lipgloss.Style) string {\n\tcellStyle := rowStyle.Copy().Inherit(column.style).Inherit(m.baseStyle)\n\n\tvar str string\n\n\tswitch column.key {\n\tcase columnKeySelect:\n\t\tif row.selected {\n\t\t\tstr = m.selectedText\n\t\t} else {\n\t\t\tstr = m.unselectedText\n\t\t}\n\tcase columnKeyOverflowRight:\n\t\tcellStyle = cellStyle.Align(lipgloss.Right)\n\t\tstr = \">\"\n\tcase columnKeyOverflowLeft:\n\t\tstr = \"<\"\n\tdefault:\n\t\tfmtString := \"%v\"\n\n\t\tvar data any\n\n\t\tif entry, exists := row.Data[column.key]; exists {\n\t\t\tdata = entry\n\n\t\t\tif column.fmtString != \"\" {\n\t\t\t\tfmtString = column.fmtString\n\t\t\t}\n\t\t} else if m.missingDataIndicator != nil {\n\t\t\tdata = m.missingDataIndicator\n\t\t} else {\n\t\t\tdata = \"\"\n\t\t}\n\n\t\tswitch entry := data.(type) {\n\t\tcase StyledCell:\n\t\t\tstr = fmt.Sprintf(fmtString, entry.Data)\n\n\t\t\tif entry.StyleFunc != nil {\n\t\t\t\tcellStyle = entry.StyleFunc(StyledCellFuncInput{\n\t\t\t\t\tColumn:         column,\n\t\t\t\t\tData:           entry.Data,\n\t\t\t\t\tRow:            row,\n\t\t\t\t\tGlobalMetadata: m.metadata,\n\t\t\t\t}).Copy().Inherit(cellStyle)\n\t\t\t} else {\n\t\t\t\tcellStyle = entry.Style.Copy().Inherit(cellStyle)\n\t\t\t}\n\t\tdefault:\n\t\t\tstr = fmt.Sprintf(fmtString, entry)\n\t\t}\n\t}\n\n\tif m.multiline {\n\t\tstr = wordwrap.String(str, column.width)\n\t\tcellStyle = cellStyle.Align(lipgloss.Top)\n\t} else {\n\t\tstr = limitStr(str, column.width)\n\t}\n\n\tcellStyle = cellStyle.Inherit(borderStyle)\n\tcellStr := cellStyle.Render(str)\n\n\treturn cellStr\n}\n\nfunc (m Model) renderRow(rowIndex int, last bool) string {\n\trow := m.GetVisibleRows()[rowIndex]\n\thighlighted := rowIndex == m.rowCursorIndex\n\n\trowStyle := row.Style.Copy()\n\n\tif m.rowStyleFunc != nil {\n\t\tstyleResult := m.rowStyleFunc(RowStyleFuncInput{\n\t\t\tIndex:         rowIndex,\n\t\t\tRow:           row,\n\t\t\tIsHighlighted: m.focused && highlighted,\n\t\t})\n\n\t\trowStyle = rowStyle.Inherit(styleResult)\n\t} else if m.focused && highlighted {\n\t\trowStyle = rowStyle.Inherit(m.highlightStyle)\n\t}\n\n\treturn m.renderRowData(row, rowStyle, last)\n}\n\nfunc (m Model) renderBlankRow(last bool) string {\n\treturn m.renderRowData(NewRow(nil), lipgloss.NewStyle(), last)\n}\n\n// This is long and could use some refactoring in the future, but not quite sure\n// how to pick it apart yet.\n//\n//nolint:funlen, cyclop\nfunc (m Model) renderRowData(row Row, rowStyle lipgloss.Style, last bool) string {\n\tnumColumns := len(m.columns)\n\n\tcolumnStrings := []string{}\n\ttotalRenderedWidth := 0\n\n\tstylesInner, stylesLast := m.styleRows()\n\n\tmaxCellHeight := 1\n\tif m.multiline {\n\t\tfor _, column := range m.columns {\n\t\t\tcellStr := m.renderRowColumnData(row, column, rowStyle, lipgloss.NewStyle())\n\t\t\tmaxCellHeight = max(maxCellHeight, lipgloss.Height(cellStr))\n\t\t}\n\t}\n\n\tfor columnIndex, column := range m.columns {\n\t\tvar borderStyle lipgloss.Style\n\t\tvar rowStyles borderStyleRow\n\n\t\tif !last {\n\t\t\trowStyles = stylesInner\n\t\t} else {\n\t\t\trowStyles = stylesLast\n\t\t}\n\t\trowStyle = rowStyle.Copy().Height(maxCellHeight)\n\n\t\tif m.horizontalScrollOffsetCol > 0 && columnIndex == m.horizontalScrollFreezeColumnsCount {\n\t\t\tvar borderStyle lipgloss.Style\n\n\t\t\tif columnIndex == 0 {\n\t\t\t\tborderStyle = rowStyles.left.Copy()\n\t\t\t} else {\n\t\t\t\tborderStyle = rowStyles.inner.Copy()\n\t\t\t}\n\n\t\t\trendered := m.renderRowColumnData(row, genOverflowColumnLeft(1), rowStyle, borderStyle)\n\n\t\t\ttotalRenderedWidth += lipgloss.Width(rendered)\n\n\t\t\tcolumnStrings = append(columnStrings, rendered)\n\t\t}\n\n\t\tif columnIndex >= m.horizontalScrollFreezeColumnsCount &&\n\t\t\tcolumnIndex < m.horizontalScrollOffsetCol+m.horizontalScrollFreezeColumnsCount {\n\t\t\tcontinue\n\t\t}\n\n\t\tif len(columnStrings) == 0 {\n\t\t\tborderStyle = rowStyles.left\n\t\t} else if columnIndex < numColumns-1 {\n\t\t\tborderStyle = rowStyles.inner\n\t\t} else {\n\t\t\tborderStyle = rowStyles.right\n\t\t}\n\n\t\tcellStr := m.renderRowColumnData(row, column, rowStyle, borderStyle)\n\n\t\tif m.maxTotalWidth != 0 {\n\t\t\trenderedWidth := lipgloss.Width(cellStr)\n\n\t\t\tconst (\n\t\t\t\tborderAdjustment = 1\n\t\t\t\toverflowColWidth = 2\n\t\t\t)\n\n\t\t\ttargetWidth := m.maxTotalWidth - overflowColWidth\n\n\t\t\tif columnIndex == len(m.columns)-1 {\n\t\t\t\t// If this is the last header, we don't need to account for the\n\t\t\t\t// overflow arrow column\n\t\t\t\ttargetWidth = m.maxTotalWidth\n\t\t\t}\n\n\t\t\tif totalRenderedWidth+renderedWidth > targetWidth {\n\t\t\t\toverflowWidth := m.maxTotalWidth - totalRenderedWidth - borderAdjustment\n\t\t\t\toverflowStyle := genOverflowStyle(rowStyles.right, overflowWidth)\n\t\t\t\toverflowColumn := genOverflowColumnRight(overflowWidth)\n\t\t\t\toverflowStr := m.renderRowColumnData(row, overflowColumn, rowStyle, overflowStyle)\n\n\t\t\t\tcolumnStrings = append(columnStrings, overflowStr)\n\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\ttotalRenderedWidth += renderedWidth\n\t\t}\n\n\t\tcolumnStrings = append(columnStrings, cellStr)\n\t}\n\n\treturn lipgloss.JoinHorizontal(lipgloss.Bottom, columnStrings...)\n}\n\n// Selected returns a copy of the row that's set to be selected or deselected.\n// The old row is not changed in-place.\nfunc (r Row) Selected(selected bool) Row {\n\tr.selected = selected\n\n\treturn r\n}\n"
  },
  {
    "path": "table/scrolling.go",
    "content": "package table\n\nfunc (m *Model) scrollRight() {\n\tif m.horizontalScrollOffsetCol < m.maxHorizontalColumnIndex {\n\t\tm.horizontalScrollOffsetCol++\n\t}\n}\n\nfunc (m *Model) scrollLeft() {\n\tif m.horizontalScrollOffsetCol > 0 {\n\t\tm.horizontalScrollOffsetCol--\n\t}\n}\n\nfunc (m *Model) recalculateLastHorizontalColumn() {\n\tif m.horizontalScrollFreezeColumnsCount >= len(m.columns) {\n\t\tm.maxHorizontalColumnIndex = 0\n\n\t\treturn\n\t}\n\n\tif m.totalWidth <= m.maxTotalWidth {\n\t\tm.maxHorizontalColumnIndex = 0\n\n\t\treturn\n\t}\n\n\tconst (\n\t\tleftOverflowWidth = 2\n\t\tborderAdjustment  = 1\n\t)\n\n\t// Always have left border\n\tvisibleWidth := borderAdjustment + leftOverflowWidth\n\n\tfor i := 0; i < m.horizontalScrollFreezeColumnsCount; i++ {\n\t\tvisibleWidth += m.columns[i].width + borderAdjustment\n\t}\n\n\tm.maxHorizontalColumnIndex = len(m.columns) - 1\n\n\t// Work backwards from the right\n\tfor i := len(m.columns) - 1; i >= m.horizontalScrollFreezeColumnsCount && visibleWidth <= m.maxTotalWidth; i-- {\n\t\tvisibleWidth += m.columns[i].width + borderAdjustment\n\n\t\tif visibleWidth <= m.maxTotalWidth {\n\t\t\tm.maxHorizontalColumnIndex = i - m.horizontalScrollFreezeColumnsCount\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "table/scrolling_fuzz_test.go",
    "content": "//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/bubbletea\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\n// This is long because of test cases\n//\n//nolint:funlen,cyclop\nfunc FuzzHorizontalScrollingStopEdgeCases(f *testing.F) {\n\tconst (\n\t\tminNameWidth = 2\n\t\tmaxNameWidth = 50\n\n\t\tminColWidth = 4\n\t\tmaxColWidth = 50\n\n\t\tminNumCols = 1\n\t\tmaxNumCols = 500\n\n\t\tminMaxWidth = 5\n\t\tmaxMaxWidth = 200\n\n\t\tborderBuffer = 4\n\t)\n\n\tf.Add(5, 3, 5, 30)\n\tf.Fuzz(func(t *testing.T, nameWidth, colWidth, numCols, maxWidth int) {\n\t\tif nameWidth < minNameWidth ||\n\t\t\tnameWidth > maxNameWidth ||\n\t\t\tnameWidth > maxWidth-colWidth ||\n\t\t\tnameWidth+colWidth+borderBuffer >= maxWidth {\n\t\t\treturn\n\t\t}\n\n\t\tif colWidth < minColWidth ||\n\t\t\tcolWidth > maxColWidth ||\n\t\t\tcolWidth >= maxWidth {\n\t\t\treturn\n\t\t}\n\n\t\tif numCols < minNumCols || numCols > maxNumCols {\n\t\t\treturn\n\t\t}\n\n\t\tif maxWidth < minMaxWidth || maxWidth > maxMaxWidth {\n\t\t\treturn\n\t\t}\n\n\t\tcols := []Column{NewColumn(\"Name\", \"Name\", nameWidth)}\n\t\tfor i := 0; i < numCols; i++ {\n\t\t\ts := fmt.Sprintf(\"%d\", i+1)\n\t\t\tcols = append(cols, NewColumn(s, s, colWidth))\n\t\t}\n\n\t\trowData := RowData{\"Name\": \"A\"}\n\n\t\tfor i := 0; i < numCols; i++ {\n\t\t\ts := fmt.Sprintf(\"%d\", i+1)\n\t\t\trowData[s] = s\n\t\t}\n\n\t\trows := []Row{NewRow(rowData)}\n\n\t\tmodel := New(cols).\n\t\t\tWithRows(rows).\n\t\t\tWithStaticFooter(\"Footer\").\n\t\t\tWithMaxTotalWidth(maxWidth).\n\t\t\tWithHorizontalFreezeColumnCount(1).\n\t\t\tFocused(true)\n\n\t\thitScrollRight := func() {\n\t\t\tmodel, _ = model.Update(tea.KeyMsg{Type: tea.KeyShiftRight})\n\t\t}\n\n\t\t// Excessive scrolling attempts to be sure\n\t\tfor i := 0; i < numCols*2; i++ {\n\t\t\thitScrollRight()\n\t\t}\n\n\t\trendered := model.View()\n\n\t\tassert.NotContains(t, rendered, \">\")\n\n\t\tif !strings.Contains(rendered, \"…\") {\n\t\t\tassert.Contains(t, rendered, fmt.Sprintf(\"%d\", numCols))\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "table/scrolling_test.go",
    "content": "package table\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/charmbracelet/bubbles/key\"\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestHorizontalScrolling(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"1\", \"1\", 4),\n\t\tNewColumn(\"2\", \"2\", 4),\n\t\tNewColumn(\"3\", \"3\", 4),\n\t\tNewColumn(\"4\", \"4\", 4),\n\t}).\n\t\tWithRows([]Row{\n\t\t\tNewRow(RowData{\n\t\t\t\t\"1\": \"x1\",\n\t\t\t\t\"2\": \"x2\",\n\t\t\t\t\"3\": \"x3\",\n\t\t\t\t\"4\": \"x4\",\n\t\t\t}),\n\t\t}).\n\t\tWithMaxTotalWidth(18).\n\t\tFocused(true)\n\n\tconst expectedTableOriginal = `┏━━━━┳━━━━┳━━━━┳━┓\n┃   1┃   2┃   3┃>┃\n┣━━━━╋━━━━╋━━━━╋━┫\n┃  x1┃  x2┃  x3┃>┃\n┗━━━━┻━━━━┻━━━━┻━┛`\n\n\tconst expectedTableAfter = `┏━┳━━━━┳━━━━┳━━━━┓\n┃<┃   2┃   3┃   4┃\n┣━╋━━━━╋━━━━╋━━━━┫\n┃<┃  x2┃  x3┃  x4┃\n┗━┻━━━━┻━━━━┻━━━━┛`\n\n\thitScrollRight := func() {\n\t\tmodel, _ = model.Update(tea.KeyMsg{Type: tea.KeyShiftRight})\n\t}\n\n\thitScrollLeft := func() {\n\t\tmodel, _ = model.Update(tea.KeyMsg{Type: tea.KeyShiftLeft})\n\t}\n\n\tassert.Equal(t, expectedTableOriginal, model.View())\n\n\thitScrollRight()\n\n\tassert.Equal(t, expectedTableAfter, model.View())\n\n\thitScrollLeft()\n\tassert.Equal(t, expectedTableOriginal, model.View())\n\n\t// Try it again, should do nothing\n\thitScrollLeft()\n\tassert.Equal(t, expectedTableOriginal, model.View())\n}\n\nfunc TestHorizontalScrollWithFooter(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"1\", \"1\", 4),\n\t\tNewColumn(\"2\", \"2\", 4),\n\t\tNewColumn(\"3\", \"3\", 4),\n\t\tNewColumn(\"4\", \"4\", 4),\n\t}).\n\t\tWithRows([]Row{\n\t\t\tNewRow(RowData{\n\t\t\t\t\"1\": \"x1\",\n\t\t\t\t\"2\": \"x2\",\n\t\t\t\t\"3\": \"x3\",\n\t\t\t\t\"4\": \"x4\",\n\t\t\t}),\n\t\t}).\n\t\tWithStaticFooter(\"Footer\").\n\t\tWithMaxTotalWidth(18).\n\t\tFocused(true)\n\n\tconst expectedTableOriginal = `┏━━━━┳━━━━┳━━━━┳━┓\n┃   1┃   2┃   3┃>┃\n┣━━━━╋━━━━╋━━━━╋━┫\n┃  x1┃  x2┃  x3┃>┃\n┣━━━━┻━━━━┻━━━━┻━┫\n┃          Footer┃\n┗━━━━━━━━━━━━━━━━┛`\n\n\tconst expectedTableAfter = `┏━┳━━━━┳━━━━┳━━━━┓\n┃<┃   2┃   3┃   4┃\n┣━╋━━━━╋━━━━╋━━━━┫\n┃<┃  x2┃  x3┃  x4┃\n┣━┻━━━━┻━━━━┻━━━━┫\n┃          Footer┃\n┗━━━━━━━━━━━━━━━━┛`\n\n\thitScrollRight := func() {\n\t\t// Try the programmatic API\n\t\tmodel = model.ScrollRight()\n\t}\n\n\thitScrollLeft := func() {\n\t\tmodel = model.ScrollLeft()\n\t}\n\n\tassert.Equal(t, expectedTableOriginal, model.View())\n\n\thitScrollRight()\n\n\tassert.Equal(t, expectedTableAfter, model.View())\n\n\thitScrollLeft()\n\tassert.Equal(t, expectedTableOriginal, model.View())\n\n\t// Try it again, should do nothing\n\thitScrollLeft()\n\tassert.Equal(t, expectedTableOriginal, model.View())\n}\n\nfunc TestHorizontalScrollingWithFooterAndFrozenCols(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"Name\", \"Name\", 4),\n\t\tNewColumn(\"1\", \"1\", 4),\n\t\tNewColumn(\"2\", \"2\", 4),\n\t\tNewColumn(\"3\", \"3\", 4),\n\t\tNewColumn(\"4\", \"4\", 4),\n\t}).\n\t\tWithRows([]Row{\n\t\t\tNewRow(RowData{\n\t\t\t\t\"Name\": \"A\",\n\t\t\t\t\"1\":    \"x1\",\n\t\t\t\t\"2\":    \"x2\",\n\t\t\t\t\"3\":    \"x3\",\n\t\t\t\t\"4\":    \"x4\",\n\t\t\t}),\n\t\t}).\n\t\tWithStaticFooter(\"Footer\").\n\t\tWithMaxTotalWidth(21).\n\t\tWithHorizontalFreezeColumnCount(1).\n\t\tFocused(true)\n\n\tconst expectedTableOriginal = `┏━━━━┳━━━━┳━━━━┳━━━━┓\n┃Name┃   1┃   2┃   >┃\n┣━━━━╋━━━━╋━━━━╋━━━━┫\n┃   A┃  x1┃  x2┃   >┃\n┣━━━━┻━━━━┻━━━━┻━━━━┫\n┃             Footer┃\n┗━━━━━━━━━━━━━━━━━━━┛`\n\n\tconst expectedTableAfter = `┏━━━━┳━┳━━━━┳━━━━┳━━┓\n┃Name┃<┃   2┃   3┃ >┃\n┣━━━━╋━╋━━━━╋━━━━╋━━┫\n┃   A┃<┃  x2┃  x3┃ >┃\n┣━━━━┻━┻━━━━┻━━━━┻━━┫\n┃             Footer┃\n┗━━━━━━━━━━━━━━━━━━━┛`\n\n\thitScrollRight := func() {\n\t\tmodel, _ = model.Update(tea.KeyMsg{Type: tea.KeyShiftRight})\n\t}\n\n\thitScrollLeft := func() {\n\t\tmodel, _ = model.Update(tea.KeyMsg{Type: tea.KeyShiftLeft})\n\t}\n\n\tassert.Equal(t, expectedTableOriginal, model.View())\n\n\thitScrollRight()\n\n\tassert.Equal(t, expectedTableAfter, model.View())\n\n\thitScrollLeft()\n\tassert.Equal(t, expectedTableOriginal, model.View())\n\n\t// Try it again, should do nothing\n\thitScrollLeft()\n\tassert.Equal(t, expectedTableOriginal, model.View())\n}\n\n// This is long due to literal strings.\nfunc TestHorizontalScrollStopsAtLastColumnBeingVisible(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"Name\", \"Name\", 4),\n\t\tNewColumn(\"1\", \"1\", 4),\n\t\tNewColumn(\"2\", \"2\", 4),\n\t\tNewColumn(\"3\", \"3\", 4),\n\t\tNewColumn(\"4\", \"4\", 4),\n\t}).\n\t\tWithRows([]Row{\n\t\t\tNewRow(RowData{\n\t\t\t\t\"Name\": \"A\",\n\t\t\t\t\"1\":    \"x1\",\n\t\t\t\t\"2\":    \"x2\",\n\t\t\t\t\"3\":    \"x3\",\n\t\t\t\t\"4\":    \"x4\",\n\t\t\t}),\n\t\t}).\n\t\tWithStaticFooter(\"Footer\").\n\t\tWithMaxTotalWidth(21).\n\t\tWithHorizontalFreezeColumnCount(1).\n\t\tFocused(true)\n\n\tconst expectedTableLeft = `┏━━━━┳━━━━┳━━━━┳━━━━┓\n┃Name┃   1┃   2┃   >┃\n┣━━━━╋━━━━╋━━━━╋━━━━┫\n┃   A┃  x1┃  x2┃   >┃\n┣━━━━┻━━━━┻━━━━┻━━━━┫\n┃             Footer┃\n┗━━━━━━━━━━━━━━━━━━━┛`\n\n\tconst expectedTableMiddle = `┏━━━━┳━┳━━━━┳━━━━┳━━┓\n┃Name┃<┃   2┃   3┃ >┃\n┣━━━━╋━╋━━━━╋━━━━╋━━┫\n┃   A┃<┃  x2┃  x3┃ >┃\n┣━━━━┻━┻━━━━┻━━━━┻━━┫\n┃             Footer┃\n┗━━━━━━━━━━━━━━━━━━━┛`\n\n\tconst expectedTableRight = `┏━━━━┳━┳━━━━┳━━━━┓\n┃Name┃<┃   3┃   4┃\n┣━━━━╋━╋━━━━╋━━━━┫\n┃   A┃<┃  x3┃  x4┃\n┣━━━━┻━┻━━━━┻━━━━┫\n┃          Footer┃\n┗━━━━━━━━━━━━━━━━┛`\n\n\thitScrollRight := func() {\n\t\tmodel, _ = model.Update(tea.KeyMsg{Type: tea.KeyShiftRight})\n\t}\n\n\tassert.Equal(t, expectedTableLeft, model.View())\n\n\thitScrollRight()\n\n\tassert.Equal(t, expectedTableMiddle, model.View())\n\n\thitScrollRight()\n\tassert.Equal(t, expectedTableRight, model.View())\n\n\t// Should no longer scroll\n\thitScrollRight()\n\tassert.Equal(t, expectedTableRight, model.View())\n}\n\nfunc TestNoScrollingWhenEntireTableIsVisible(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"Name\", \"Name\", 4),\n\t\tNewColumn(\"1\", \"1\", 4),\n\t\tNewColumn(\"2\", \"2\", 4),\n\t\tNewColumn(\"3\", \"3\", 4),\n\t}).\n\t\tWithRows([]Row{\n\t\t\tNewRow(RowData{\n\t\t\t\t\"Name\": \"A\",\n\t\t\t\t\"1\":    \"x1\",\n\t\t\t\t\"2\":    \"x2\",\n\t\t\t\t\"3\":    \"x3\",\n\t\t\t}),\n\t\t}).\n\t\tWithStaticFooter(\"Footer\").\n\t\tWithMaxTotalWidth(21).\n\t\tWithHorizontalFreezeColumnCount(1).\n\t\tFocused(true)\n\n\tconst expectedTable = `┏━━━━┳━━━━┳━━━━┳━━━━┓\n┃Name┃   1┃   2┃   3┃\n┣━━━━╋━━━━╋━━━━╋━━━━┫\n┃   A┃  x1┃  x2┃  x3┃\n┣━━━━┻━━━━┻━━━━┻━━━━┫\n┃             Footer┃\n┗━━━━━━━━━━━━━━━━━━━┛`\n\n\thitScrollRight := func() {\n\t\tmodel, _ = model.Update(tea.KeyMsg{Type: tea.KeyShiftRight})\n\t}\n\n\tassert.Equal(t, expectedTable, model.View())\n\n\thitScrollRight()\n\n\tassert.Equal(t, expectedTable, model.View())\n}\n\n// This is long because of test cases\n//\n//nolint:funlen\nfunc TestHorizontalScrollingStopEdgeCases(t *testing.T) {\n\ttests := []struct {\n\t\tnumCols      int\n\t\tnameWidth    int\n\t\tcolWidth     int\n\t\tmaxWidth     int\n\t\texpectedCols []int\n\t}{\n\t\t{\n\t\t\tnumCols:   8,\n\t\t\tnameWidth: 5,\n\t\t\tcolWidth:  3,\n\t\t\tmaxWidth:  30,\n\t\t},\n\t\t{\n\t\t\tnumCols:      8,\n\t\t\tnameWidth:    5,\n\t\t\tcolWidth:     3,\n\t\t\tmaxWidth:     20,\n\t\t\texpectedCols: []int{7, 8},\n\t\t},\n\t\t{\n\t\t\tnumCols:   6,\n\t\t\tnameWidth: 5,\n\t\t\tcolWidth:  3,\n\t\t\tmaxWidth:  30,\n\t\t},\n\t\t{\n\t\t\tnumCols:   50,\n\t\t\tnameWidth: 20,\n\t\t\tcolWidth:  6,\n\t\t\tmaxWidth:  31,\n\t\t},\n\t}\n\n\tfor i, test := range tests {\n\t\tt.Run(fmt.Sprintf(\"%d\", i), func(t *testing.T) {\n\t\t\tcols := []Column{NewColumn(\"Name\", \"Name\", test.nameWidth)}\n\t\t\tfor i := 0; i < test.numCols; i++ {\n\t\t\t\ts := fmt.Sprintf(\"%d\", i+1)\n\t\t\t\tcols = append(cols, NewColumn(s, s, test.colWidth))\n\t\t\t}\n\n\t\t\trowData := RowData{\"Name\": \"A\"}\n\n\t\t\tfor i := 0; i < test.numCols; i++ {\n\t\t\t\ts := fmt.Sprintf(\"%d\", i+1)\n\t\t\t\trowData[s] = s\n\t\t\t}\n\n\t\t\trows := []Row{NewRow(rowData)}\n\n\t\t\tmodel := New(cols).\n\t\t\t\tWithRows(rows).\n\t\t\t\tWithStaticFooter(\"Footer\").\n\t\t\t\tWithMaxTotalWidth(test.maxWidth).\n\t\t\t\tWithHorizontalFreezeColumnCount(1).\n\t\t\t\tFocused(true)\n\n\t\t\thitScrollRight := func() {\n\t\t\t\tmodel, _ = model.Update(tea.KeyMsg{Type: tea.KeyShiftRight})\n\t\t\t}\n\n\t\t\t// Excessive scrolling attempts to be sure\n\t\t\tfor i := 0; i < test.numCols*2; i++ {\n\t\t\t\thitScrollRight()\n\t\t\t}\n\n\t\t\trendered := model.View()\n\n\t\t\tassert.NotContains(t, rendered, \">\")\n\t\t\tassert.Contains(t, rendered, fmt.Sprintf(\"%d\", test.numCols))\n\n\t\t\tfor _, expected := range test.expectedCols {\n\t\t\t\tassert.Contains(t, rendered, fmt.Sprintf(\"%d\", expected), \"Missing expected column\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHorizontalScrollingWithCustomKeybind(t *testing.T) {\n\tkeymap := DefaultKeyMap()\n\n\t// These intentionally overlap with the keybinds for paging, to ensure\n\t// that conflicts can live together\n\tkeymap.ScrollRight = key.NewBinding(key.WithKeys(\"right\"))\n\tkeymap.ScrollLeft = key.NewBinding(key.WithKeys(\"left\"))\n\n\tmodel := New([]Column{\n\t\tNewColumn(\"1\", \"1\", 4),\n\t\tNewColumn(\"2\", \"2\", 4),\n\t\tNewColumn(\"3\", \"3\", 4),\n\t\tNewColumn(\"4\", \"4\", 4),\n\t}).\n\t\tWithRows([]Row{\n\t\t\tNewRow(RowData{\n\t\t\t\t\"1\": \"x1\",\n\t\t\t\t\"2\": \"x2\",\n\t\t\t\t\"3\": \"x3\",\n\t\t\t\t\"4\": \"x4\",\n\t\t\t}),\n\t\t}).\n\t\tWithKeyMap(keymap).\n\t\tWithMaxTotalWidth(18).\n\t\tFocused(true)\n\n\tconst expectedTableOriginal = `┏━━━━┳━━━━┳━━━━┳━┓\n┃   1┃   2┃   3┃>┃\n┣━━━━╋━━━━╋━━━━╋━┫\n┃  x1┃  x2┃  x3┃>┃\n┗━━━━┻━━━━┻━━━━┻━┛`\n\n\tconst expectedTableAfter = `┏━┳━━━━┳━━━━┳━━━━┓\n┃<┃   2┃   3┃   4┃\n┣━╋━━━━╋━━━━╋━━━━┫\n┃<┃  x2┃  x3┃  x4┃\n┗━┻━━━━┻━━━━┻━━━━┛`\n\n\thitScrollRight := func() {\n\t\tmodel, _ = model.Update(tea.KeyMsg{Type: tea.KeyRight})\n\t}\n\n\thitScrollLeft := func() {\n\t\tmodel, _ = model.Update(tea.KeyMsg{Type: tea.KeyLeft})\n\t}\n\n\tassert.Equal(t, expectedTableOriginal, model.View())\n\n\thitScrollRight()\n\n\tassert.Equal(t, expectedTableAfter, model.View())\n\n\thitScrollLeft()\n\tassert.Equal(t, expectedTableOriginal, model.View())\n\n\t// Try it again, should do nothing\n\thitScrollLeft()\n\tassert.Equal(t, expectedTableOriginal, model.View())\n}\n"
  },
  {
    "path": "table/sort.go",
    "content": "package table\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n)\n\n// SortDirection indicates whether a column should sort by ascending or descending.\ntype SortDirection int\n\nconst (\n\t// SortDirectionAsc indicates the column should be in ascending order.\n\tSortDirectionAsc SortDirection = iota\n\n\t// SortDirectionDesc indicates the column should be in descending order.\n\tSortDirectionDesc\n)\n\n// SortColumn describes which column should be sorted and how.\ntype SortColumn struct {\n\tColumnKey string\n\tDirection SortDirection\n}\n\n// SortByAsc sets the main sorting column to the given key, in ascending order.\n// If a previous sort was used, it is replaced by the given column each time\n// this function is called.  Values are sorted as numbers if possible, or just\n// as simple string comparisons if not numbers.\nfunc (m Model) SortByAsc(columnKey string) Model {\n\tm.sortOrder = []SortColumn{\n\t\t{\n\t\t\tColumnKey: columnKey,\n\t\t\tDirection: SortDirectionAsc,\n\t\t},\n\t}\n\n\tm.visibleRowCacheUpdated = false\n\n\treturn m\n}\n\n// SortByDesc sets the main sorting column to the given key, in descending order.\n// If a previous sort was used, it is replaced by the given column each time\n// this function is called.  Values are sorted as numbers if possible, or just\n// as simple string comparisons if not numbers.\nfunc (m Model) SortByDesc(columnKey string) Model {\n\tm.sortOrder = []SortColumn{\n\t\t{\n\t\t\tColumnKey: columnKey,\n\t\t\tDirection: SortDirectionDesc,\n\t\t},\n\t}\n\n\tm.visibleRowCacheUpdated = false\n\n\treturn m\n}\n\n// ThenSortByAsc provides a secondary sort after the first, in ascending order.\n// Can be chained multiple times, applying to smaller subgroups each time.\nfunc (m Model) ThenSortByAsc(columnKey string) Model {\n\tm.sortOrder = append([]SortColumn{\n\t\t{\n\t\t\tColumnKey: columnKey,\n\t\t\tDirection: SortDirectionAsc,\n\t\t},\n\t}, m.sortOrder...)\n\n\tm.visibleRowCacheUpdated = false\n\n\treturn m\n}\n\n// ThenSortByDesc provides a secondary sort after the first, in descending order.\n// Can be chained multiple times, applying to smaller subgroups each time.\nfunc (m Model) ThenSortByDesc(columnKey string) Model {\n\tm.sortOrder = append([]SortColumn{\n\t\t{\n\t\t\tColumnKey: columnKey,\n\t\t\tDirection: SortDirectionDesc,\n\t\t},\n\t}, m.sortOrder...)\n\n\tm.visibleRowCacheUpdated = false\n\n\treturn m\n}\n\ntype sortableTable struct {\n\trows     []Row\n\tbyColumn SortColumn\n}\n\nfunc (s *sortableTable) Len() int {\n\treturn len(s.rows)\n}\n\nfunc (s *sortableTable) Swap(i, j int) {\n\told := s.rows[i]\n\ts.rows[i] = s.rows[j]\n\ts.rows[j] = old\n}\n\nfunc (s *sortableTable) extractString(i int, column string) string {\n\tiData, exists := s.rows[i].Data[column]\n\n\tif !exists {\n\t\treturn \"\"\n\t}\n\n\tswitch iData := iData.(type) {\n\tcase StyledCell:\n\t\treturn fmt.Sprintf(\"%v\", iData.Data)\n\n\tcase string:\n\t\treturn iData\n\n\tdefault:\n\t\treturn fmt.Sprintf(\"%v\", iData)\n\t}\n}\n\nfunc (s *sortableTable) extractNumber(i int, column string) (float64, bool) {\n\tiData, exists := s.rows[i].Data[column]\n\n\tif !exists {\n\t\treturn 0, false\n\t}\n\n\treturn asNumber(iData)\n}\n\nfunc (s *sortableTable) Less(first, second int) bool {\n\tfirstNum, firstNumIsValid := s.extractNumber(first, s.byColumn.ColumnKey)\n\tsecondNum, secondNumIsValid := s.extractNumber(second, s.byColumn.ColumnKey)\n\n\tif firstNumIsValid && secondNumIsValid {\n\t\tif s.byColumn.Direction == SortDirectionAsc {\n\t\t\treturn firstNum < secondNum\n\t\t}\n\n\t\treturn firstNum > secondNum\n\t}\n\n\tfirstVal := s.extractString(first, s.byColumn.ColumnKey)\n\tsecondVal := s.extractString(second, s.byColumn.ColumnKey)\n\n\tif s.byColumn.Direction == SortDirectionAsc {\n\t\treturn firstVal < secondVal\n\t}\n\n\treturn firstVal > secondVal\n}\n\nfunc getSortedRows(sortOrder []SortColumn, rows []Row) []Row {\n\tvar sortedRows []Row\n\tif len(sortOrder) == 0 {\n\t\tsortedRows = rows\n\n\t\treturn sortedRows\n\t}\n\n\tsortedRows = make([]Row, len(rows))\n\tcopy(sortedRows, rows)\n\n\tfor _, byColumn := range sortOrder {\n\t\tsorted := &sortableTable{\n\t\t\trows:     sortedRows,\n\t\t\tbyColumn: byColumn,\n\t\t}\n\n\t\tsort.Stable(sorted)\n\n\t\tsortedRows = sorted.rows\n\t}\n\n\treturn sortedRows\n}\n"
  },
  {
    "path": "table/sort_test.go",
    "content": "package table\n\nimport (\n\t\"testing\"\n\n\t\"github.com/charmbracelet/lipgloss\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestSortSingleColumnAscAndDesc(t *testing.T) {\n\tconst idColKey = \"id\"\n\n\t// Check mixing types\n\ttype someType string\n\n\trows := []Row{\n\t\tNewRow(RowData{idColKey: someType(\"b\")}),\n\t\tNewRow(RowData{idColKey: NewStyledCell(\"c\", lipgloss.NewStyle().Bold(true))}),\n\t\tNewRow(RowData{idColKey: \"a\"}),\n\t\t// Missing data\n\t\tNewRow(RowData{}),\n\t}\n\n\tmodel := New([]Column{\n\t\tNewColumn(idColKey, \"ID\", 3),\n\t}).WithRows(rows).SortByAsc(idColKey)\n\n\tassertOrder := func(expectedList []string) {\n\t\tfor index, expected := range expectedList {\n\t\t\tidVal, ok := model.GetVisibleRows()[index].Data[idColKey]\n\n\t\t\tif expected != \"\" {\n\t\t\t\tassert.True(t, ok)\n\t\t\t} else {\n\t\t\t\tassert.False(t, ok)\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tswitch idVal := idVal.(type) {\n\t\t\tcase string:\n\t\t\t\tassert.Equal(t, expected, idVal)\n\n\t\t\tcase someType:\n\t\t\t\tassert.Equal(t, expected, string(idVal))\n\n\t\t\tcase StyledCell:\n\t\t\t\tassert.Equal(t, expected, idVal.Data)\n\n\t\t\tdefault:\n\t\t\t\tassert.Fail(t, \"Unknown type\")\n\t\t\t}\n\t\t}\n\t}\n\n\tassert.Len(t, model.GetVisibleRows(), len(rows))\n\tassertOrder([]string{\"\", \"a\", \"b\", \"c\"})\n\n\tmodel = model.SortByDesc(idColKey)\n\n\tassertOrder([]string{\"c\", \"b\", \"a\", \"\"})\n}\n\nfunc TestSortSingleColumnIntsAsc(t *testing.T) {\n\tconst idColKey = \"id\"\n\n\trows := []Row{\n\t\tNewRow(RowData{idColKey: 13}),\n\t\tNewRow(RowData{idColKey: NewStyledCell(1, lipgloss.NewStyle().Bold(true))}),\n\t\tNewRow(RowData{idColKey: 2}),\n\t}\n\n\tmodel := New([]Column{\n\t\tNewColumn(idColKey, \"ID\", 3),\n\t}).WithRows(rows).SortByAsc(idColKey)\n\n\tassertOrder := func(expectedList []int) {\n\t\tfor index, expected := range expectedList {\n\t\t\tidVal, ok := model.GetVisibleRows()[index].Data[idColKey]\n\n\t\t\tassert.True(t, ok)\n\n\t\t\tswitch idVal := idVal.(type) {\n\t\t\tcase int:\n\t\t\t\tassert.Equal(t, expected, idVal)\n\n\t\t\tcase StyledCell:\n\t\t\t\tassert.Equal(t, expected, idVal.Data)\n\n\t\t\tdefault:\n\t\t\t\tassert.Fail(t, \"Unknown type\")\n\t\t\t}\n\t\t}\n\t}\n\n\tassert.Len(t, model.GetVisibleRows(), len(rows))\n\tassertOrder([]int{1, 2, 13})\n}\n\nfunc TestSortTwoColumnsAscDescMix(t *testing.T) {\n\tconst (\n\t\tnameKey  = \"name\"\n\t\tscoreKey = \"score\"\n\t)\n\n\tmakeRow := func(name string, score int) Row {\n\t\treturn NewRow(RowData{\n\t\t\tnameKey:  name,\n\t\t\tscoreKey: score,\n\t\t})\n\t}\n\n\tmodel := New([]Column{\n\t\tNewColumn(nameKey, \"Name\", 8),\n\t\tNewColumn(scoreKey, \"Score\", 8),\n\t}).WithRows([]Row{\n\t\tmakeRow(\"c\", 50),\n\t\tmakeRow(\"a\", 75),\n\t\tmakeRow(\"b\", 101),\n\t\tmakeRow(\"a\", 100),\n\t}).SortByAsc(nameKey).ThenSortByDesc(scoreKey)\n\n\tassertVals := func(index int, name string, score int) {\n\t\tactualName, ok := model.GetVisibleRows()[index].Data[nameKey].(string)\n\t\tassert.True(t, ok)\n\n\t\tactualScore, ok := model.GetVisibleRows()[index].Data[scoreKey].(int)\n\t\tassert.True(t, ok)\n\n\t\tassert.Equal(t, name, actualName)\n\t\tassert.Equal(t, score, actualScore)\n\t}\n\n\tassert.Len(t, model.GetVisibleRows(), 4)\n\n\tassertVals(0, \"a\", 100)\n\tassertVals(1, \"a\", 75)\n\tassertVals(2, \"b\", 101)\n\tassertVals(3, \"c\", 50)\n\n\tmodel = model.SortByDesc(nameKey).ThenSortByAsc(scoreKey)\n\n\tassertVals(0, \"c\", 50)\n\tassertVals(1, \"b\", 101)\n\tassertVals(2, \"a\", 75)\n\tassertVals(3, \"a\", 100)\n}\n\nfunc TestGetSortedRows(t *testing.T) {\n\tsortColumns := []SortColumn{\n\t\t{\n\t\t\tColumnKey: \"cb\",\n\t\t\tDirection: SortDirectionDesc,\n\t\t},\n\t\t{\n\t\t\tColumnKey: \"ca\",\n\t\t\tDirection: SortDirectionAsc,\n\t\t},\n\t}\n\trows := getSortedRows(sortColumns, []Row{\n\t\tNewRow(RowData{\n\t\t\t\"ca\": \"2\",\n\t\t\t\"cb\": \"t-1\",\n\t\t}),\n\t\tNewRow(RowData{\n\t\t\t\"ca\": \"1\",\n\t\t\t\"cb\": \"t-2\",\n\t\t}),\n\t\tNewRow(RowData{\n\t\t\t\"ca\": \"3\",\n\t\t\t\"cb\": \"t-3\",\n\t\t}),\n\t\tNewRow(RowData{\n\t\t\t\"ca\": \"3\",\n\t\t\t\"cb\": \"t-2\",\n\t\t}),\n\t})\n\tassert.Len(t, rows, 4)\n\tassert.Equal(t, \"1\", rows[0].Data[\"ca\"])\n\tassert.Equal(t, \"2\", rows[1].Data[\"ca\"])\n\tassert.Equal(t, \"3\", rows[2].Data[\"ca\"])\n\tassert.Equal(t, \"3\", rows[3].Data[\"ca\"])\n\n\tassert.Equal(t, \"t-2\", rows[0].Data[\"cb\"])\n\tassert.Equal(t, \"t-1\", rows[1].Data[\"cb\"])\n\tassert.Equal(t, \"t-3\", rows[2].Data[\"cb\"])\n\tassert.Equal(t, \"t-2\", rows[3].Data[\"cb\"])\n}\n"
  },
  {
    "path": "table/strlimit.go",
    "content": "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 limitStr(str string, maxLen int) string {\n\tif maxLen == 0 {\n\t\treturn \"\"\n\t}\n\n\tnewLineIndex := strings.Index(str, \"\\n\")\n\tif newLineIndex > -1 {\n\t\tstr = str[:newLineIndex] + \"…\"\n\t}\n\n\tif ansi.PrintableRuneWidth(str) > maxLen {\n\t\t// #nosec: G115\n\t\treturn truncate.StringWithTail(str, uint(maxLen), \"…\")\n\t}\n\n\treturn str\n}\n"
  },
  {
    "path": "table/strlimit_test.go",
    "content": "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 function is only long because of repetitive test definitions, this is fine\n//\n//nolint:funlen\nfunc TestLimitStr(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\tmax      int\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"Short\",\n\t\t\tinput:    \"Hello\",\n\t\t\tmax:      50,\n\t\t\texpected: \"Hello\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Close\",\n\t\t\tinput:    \"Hello\",\n\t\t\tmax:      6,\n\t\t\texpected: \"Hello\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Equal\",\n\t\t\tinput:    \"Hello\",\n\t\t\tmax:      5,\n\t\t\texpected: \"Hello\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Shorter\",\n\t\t\tinput:    \"Hello this is a really long string\",\n\t\t\tmax:      8,\n\t\t\texpected: \"Hello t…\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Zero max\",\n\t\t\tinput:    \"Hello\",\n\t\t\tmax:      0,\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Unicode width\",\n\t\t\tinput:    \"✓\",\n\t\t\tmax:      1,\n\t\t\texpected: \"✓\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Unicode truncated\",\n\t\t\tinput:    \"✓✓✓\",\n\t\t\tmax:      2,\n\t\t\texpected: \"✓…\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Unicode japenese equal\",\n\t\t\tinput:    \"直立\",\n\t\t\tmax:      5,\n\t\t\texpected: \"直立\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Unicode japenese truncated\",\n\t\t\tinput:    \"直立した恐\",\n\t\t\tmax:      5,\n\t\t\texpected: \"直立…\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Multiline truncated\",\n\t\t\tinput:    \"hi\\nall\",\n\t\t\tmax:      5,\n\t\t\texpected: \"hi…\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Multiline with exact max width\",\n\t\t\tinput:    \"hello\\nall\",\n\t\t\tmax:      5,\n\t\t\texpected: \"hell…\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Embedded ANSI control sequences with exact max width\",\n\t\t\tinput:    \"\\x1b[31;41mtest\\x1b[0m\",\n\t\t\tmax:      4,\n\t\t\texpected: \"\\x1b[31;41mtest\\x1b[0m\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Embedded ANSI control sequences with truncation\",\n\t\t\tinput:    \"\\x1b[31;41mte\\x1b[0m\\x1b[0m\\x1b[0mst\",\n\t\t\tmax:      3,\n\t\t\texpected: \"\\x1b[31;41mte\\x1b[0m\\x1b[0m\\x1b[0m…\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\toutput := limitStr(test.input, test.max)\n\n\t\t\tassert.Equal(t, test.expected, output)\n\t\t\tassert.LessOrEqual(t, ansi.PrintableRuneWidth(output), test.max)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "table/update.go",
    "content": "package table\n\nimport (\n\t\"github.com/charmbracelet/bubbles/key\"\n\ttea \"github.com/charmbracelet/bubbletea\"\n)\n\nfunc (m *Model) moveHighlightUp() {\n\tm.rowCursorIndex--\n\n\tif m.rowCursorIndex < 0 {\n\t\tm.rowCursorIndex = len(m.GetVisibleRows()) - 1\n\t}\n\n\tm.currentPage = m.expectedPageForRowIndex(m.rowCursorIndex)\n}\n\nfunc (m *Model) moveHighlightDown() {\n\tm.rowCursorIndex++\n\n\tif m.rowCursorIndex >= len(m.GetVisibleRows()) {\n\t\tm.rowCursorIndex = 0\n\t}\n\n\tm.currentPage = m.expectedPageForRowIndex(m.rowCursorIndex)\n}\n\nfunc (m *Model) toggleSelect() {\n\tif !m.selectableRows || len(m.GetVisibleRows()) == 0 {\n\t\treturn\n\t}\n\n\trows := m.GetVisibleRows()\n\n\trowID := rows[m.rowCursorIndex].id\n\n\tcurrentSelectedState := false\n\n\tfor i := range m.rows {\n\t\tif m.rows[i].id == rowID {\n\t\t\tcurrentSelectedState = m.rows[i].selected\n\t\t\tm.rows[i].selected = !m.rows[i].selected\n\t\t}\n\t}\n\n\tm.visibleRowCacheUpdated = false\n\n\tm.appendUserEvent(UserEventRowSelectToggled{\n\t\tRowIndex:   m.rowCursorIndex,\n\t\tIsSelected: !currentSelectedState,\n\t})\n}\n\nfunc (m Model) updateFilterTextInput(msg tea.Msg) (Model, tea.Cmd) {\n\tvar cmd tea.Cmd\n\tswitch msg := msg.(type) {\n\tcase tea.KeyMsg:\n\t\tif key.Matches(msg, m.keyMap.FilterBlur) {\n\t\t\tm.filterTextInput.Blur()\n\t\t}\n\t}\n\tm.filterTextInput, cmd = m.filterTextInput.Update(msg)\n\tm.pageFirst()\n\tm.visibleRowCacheUpdated = false\n\n\treturn m, cmd\n}\n\n// This is a series of Matches tests with minimal logic\n//\n//nolint:cyclop\nfunc (m *Model) handleKeypress(msg tea.KeyMsg) {\n\tpreviousRowIndex := m.rowCursorIndex\n\n\tif key.Matches(msg, m.keyMap.RowDown) {\n\t\tm.moveHighlightDown()\n\t}\n\n\tif key.Matches(msg, m.keyMap.RowUp) {\n\t\tm.moveHighlightUp()\n\t}\n\n\tif key.Matches(msg, m.keyMap.RowSelectToggle) {\n\t\tm.toggleSelect()\n\t}\n\n\tif key.Matches(msg, m.keyMap.PageDown) {\n\t\tm.pageDown()\n\t}\n\n\tif key.Matches(msg, m.keyMap.PageUp) {\n\t\tm.pageUp()\n\t}\n\n\tif key.Matches(msg, m.keyMap.PageFirst) {\n\t\tm.pageFirst()\n\t}\n\n\tif key.Matches(msg, m.keyMap.PageLast) {\n\t\tm.pageLast()\n\t}\n\n\tif key.Matches(msg, m.keyMap.Filter) {\n\t\tm.filterTextInput.Focus()\n\t\tm.appendUserEvent(UserEventFilterInputFocused{})\n\t}\n\n\tif key.Matches(msg, m.keyMap.FilterClear) {\n\t\tm.visibleRowCacheUpdated = false\n\t\tm.filterTextInput.Reset()\n\t}\n\n\tif key.Matches(msg, m.keyMap.ScrollRight) {\n\t\tm.scrollRight()\n\t}\n\n\tif key.Matches(msg, m.keyMap.ScrollLeft) {\n\t\tm.scrollLeft()\n\t}\n\n\tif m.rowCursorIndex != previousRowIndex {\n\t\tm.appendUserEvent(UserEventHighlightedIndexChanged{\n\t\t\tPreviousRowIndex: previousRowIndex,\n\t\t\tSelectedRowIndex: m.rowCursorIndex,\n\t\t})\n\t}\n}\n\n// Update responds to input from the user or other messages from Bubble Tea.\nfunc (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {\n\tm.clearUserEvents()\n\n\tif !m.focused {\n\t\treturn m, nil\n\t}\n\n\tif m.filterTextInput.Focused() {\n\t\tvar cmd tea.Cmd\n\t\tm, cmd = m.updateFilterTextInput(msg)\n\n\t\tif !m.filterTextInput.Focused() {\n\t\t\tm.appendUserEvent(UserEventFilterInputUnfocused{})\n\t\t}\n\n\t\treturn m, cmd\n\t}\n\n\tswitch msg := msg.(type) {\n\tcase tea.KeyMsg:\n\t\tm.handleKeypress(msg)\n\t}\n\n\treturn m, nil\n}\n"
  },
  {
    "path": "table/update_test.go",
    "content": "package table\n\nimport (\n\t\"testing\"\n\n\t\"github.com/charmbracelet/bubbles/key\"\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestUnfocusedDoesntMove(t *testing.T) {\n\tcols := []Column{\n\t\tNewColumn(\"id\", \"ID\", 3),\n\t}\n\n\tmodel := New(cols).WithRows([]Row{\n\t\tNewRow(RowData{\n\t\t\t\"id\": \"first\",\n\t\t}),\n\t\tNewRow(RowData{\n\t\t\t\"id\": \"second\",\n\t\t}),\n\t})\n\n\tmodel, _ = model.Update(tea.KeyMsg{\n\t\tType: tea.KeyUp,\n\t})\n\n\thighlighted := model.HighlightedRow()\n\n\tid, ok := highlighted.Data[\"id\"].(string)\n\n\tassert.True(t, ok, \"Failed to convert to string\")\n\n\tassert.Equal(t, \"first\", id, \"Should still be on first row\")\n}\n\nfunc TestPageKeysDoNothingWhenNoPages(t *testing.T) {\n\tcols := []Column{\n\t\tNewColumn(\"id\", \"ID\", 3),\n\t}\n\n\tmodel := New(cols).WithRows([]Row{\n\t\tNewRow(RowData{\n\t\t\t\"id\": \"first\",\n\t\t}),\n\t\tNewRow(RowData{\n\t\t\t\"id\": \"second\",\n\t\t}),\n\t\tNewRow(RowData{\n\t\t\t\"id\": \"third\",\n\t\t}),\n\t}).Focused(true)\n\n\tpageMoveKeys := []tea.Msg{\n\t\ttea.KeyMsg{Type: tea.KeyLeft},\n\t\ttea.KeyMsg{Type: tea.KeyRight},\n\t\ttea.KeyMsg{Type: tea.KeyHome},\n\t\ttea.KeyMsg{Type: tea.KeyEnd},\n\t}\n\n\tcheckNoMove := func() string {\n\t\tstr, ok := model.HighlightedRow().Data[\"id\"].(string)\n\n\t\tassert.True(t, ok, \"Failed to convert to string\")\n\n\t\tassert.Equal(t, \"first\", str, \"Shouldn't move\")\n\n\t\treturn str\n\t}\n\n\tfor _, msg := range pageMoveKeys {\n\t\tmodel, _ = model.Update(msg)\n\t\tcheckNoMove()\n\t}\n}\n\n// This is a long test with a lot of movement keys pressed, that's okay because\n// it's simply repetitive and tracking the same kind of state change many times\n//\n//nolint:funlen\nfunc TestFocusedMovesWhenMoveKeysPressedPaged(t *testing.T) {\n\tcols := []Column{\n\t\tNewColumn(\"id\", \"ID\", 3),\n\t}\n\n\tmodel := New(cols).WithRows([]Row{\n\t\tNewRow(RowData{\n\t\t\t\"id\": \"first\",\n\t\t}),\n\t\tNewRow(RowData{\n\t\t\t\"id\": \"second\",\n\t\t}),\n\t\tNewRow(RowData{\n\t\t\t\"id\": \"third\",\n\t\t}),\n\t}).Focused(true).WithPageSize(2)\n\n\t// Note that this is assuming default keymap\n\tkeyUp := tea.KeyMsg{Type: tea.KeyUp}\n\tkeyDown := tea.KeyMsg{Type: tea.KeyDown}\n\tkeyLeft := tea.KeyMsg{Type: tea.KeyLeft}\n\tkeyRight := tea.KeyMsg{Type: tea.KeyRight}\n\tkeyHome := tea.KeyMsg{Type: tea.KeyHome}\n\tkeyEnd := tea.KeyMsg{Type: tea.KeyEnd}\n\n\tcurID := func() string {\n\t\tstr, ok := model.HighlightedRow().Data[\"id\"].(string)\n\n\t\tassert.True(t, ok, \"Failed to convert to string\")\n\n\t\treturn str\n\t}\n\n\tassert.Equal(t, \"first\", curID(), \"Should start on first row\")\n\n\tmodel, _ = model.Update(keyDown)\n\tassert.Equal(t, \"second\", curID(), \"Default key down should move down a row\")\n\n\tmodel, _ = model.Update(keyUp)\n\tassert.Equal(t, \"first\", curID(), \"Should move back up\")\n\n\tmodel, _ = model.Update(keyUp)\n\tassert.Equal(t, \"third\", curID(), \"Moving up from top should wrap to bottom\")\n\n\tmodel, _ = model.Update(keyDown)\n\tassert.Equal(t, \"first\", curID(), \"Moving down from bottom should wrap to top\")\n\n\tmodel, _ = model.Update(keyRight)\n\tassert.Equal(t, \"third\", curID(), \"Moving right should move to second page\")\n\n\tmodel, _ = model.Update(keyRight)\n\tassert.Equal(t, \"first\", curID(), \"Moving right again should move to first page\")\n\n\tmodel, _ = model.Update(keyLeft)\n\tassert.Equal(t, \"third\", curID(), \"Moving left should move to last page\")\n\n\tmodel, _ = model.Update(keyLeft)\n\tassert.Equal(t, \"first\", curID(), \"Moving left should move back to first page\")\n\n\tmodel, _ = model.Update(keyDown)\n\tassert.Equal(t, \"second\", curID(), \"Should be back down to second row\")\n\n\tmodel, _ = model.Update(keyHome)\n\tassert.Equal(t, \"first\", curID(), \"Hitting home should go to first page and select first row\")\n\n\tmodel, _ = model.Update(keyHome)\n\tassert.Equal(t, \"first\", curID(), \"Hitting home a second time should not move pages\")\n\n\tmodel, _ = model.Update(keyEnd)\n\tassert.Equal(t, \"third\", curID(), \"Hitting end should move to last page\")\n\n\tmodel, _ = model.Update(keyEnd)\n\tassert.Equal(t, \"third\", curID(), \"Hitting end a second time should not move pages\")\n\n\t// Disable pagination wrapping and ensure it sticks\n\tmodel = model.WithPaginationWrapping(false)\n\tmodel, _ = model.Update(keyRight)\n\tassert.Equal(t, \"third\", curID(), \"Did not stay on last page, may have wrapped\")\n\n\tmodel, _ = model.Update(keyHome)\n\tmodel, _ = model.Update(keyLeft)\n\tassert.Equal(t, \"first\", curID(), \"Did not stay on first page, may have wrapped\")\n}\n\nfunc TestFocusedMovesWithCustomKeyMap(t *testing.T) {\n\tcols := []Column{\n\t\tNewColumn(\"id\", \"ID\", 3),\n\t}\n\n\tcustomKeys := KeyMap{\n\t\tRowUp:   key.NewBinding(key.WithKeys(\"ctrl+a\")),\n\t\tRowDown: key.NewBinding(key.WithKeys(\"ctrl+b\")),\n\n\t\tRowSelectToggle: key.NewBinding(key.WithKeys(\"ctrl+c\")),\n\t}\n\n\tmodel := New(cols).WithRows([]Row{\n\t\tNewRow(RowData{\n\t\t\t\"id\": \"first\",\n\t\t}),\n\t\tNewRow(RowData{\n\t\t\t\"id\": \"second\",\n\t\t}),\n\t\tNewRow(RowData{\n\t\t\t\"id\": \"third\",\n\t\t}),\n\t}).Focused(true).WithKeyMap(customKeys)\n\n\tkeyUp := tea.KeyMsg{Type: tea.KeyUp}\n\tkeyDown := tea.KeyMsg{Type: tea.KeyDown}\n\tkeyCtrlA := tea.KeyMsg{Type: tea.KeyCtrlA}\n\tkeyCtrlB := tea.KeyMsg{Type: tea.KeyCtrlB}\n\n\tassert.Equal(t, \"ctrl+a\", keyCtrlA.String(), \"Test sanity check failed for ctrl+a\")\n\tassert.Equal(t, \"ctrl+b\", keyCtrlB.String(), \"Test sanity check failed for ctrl+b\")\n\n\tcurID := func() string {\n\t\tstr, ok := model.HighlightedRow().Data[\"id\"].(string)\n\n\t\tassert.True(t, ok, \"Failed to convert to string\")\n\n\t\treturn str\n\t}\n\n\tassert.Equal(t, \"first\", curID(), \"Should start on first row\")\n\n\tmodel, _ = model.Update(keyDown)\n\tassert.Equal(t, \"first\", curID(), \"Down arrow should do nothing\")\n\n\tmodel, _ = model.Update(keyCtrlB)\n\tassert.Equal(t, \"second\", curID(), \"Custom key map for down failed\")\n\n\tmodel, _ = model.Update(keyUp)\n\tassert.Equal(t, \"second\", curID(), \"Up arrow should do nothing\")\n\n\tmodel, _ = model.Update(keyCtrlA)\n\tassert.Equal(t, \"first\", curID(), \"Custom key map for up failed\")\n}\n\nfunc TestSelectingRowWhenTableUnselectableDoesNothing(t *testing.T) {\n\tcols := []Column{\n\t\tNewColumn(\"id\", \"ID\", 3),\n\t}\n\n\tmodel := New(cols).WithRows([]Row{\n\t\tNewRow(RowData{\n\t\t\t\"id\": \"first\",\n\t\t}),\n\t\tNewRow(RowData{\n\t\t\t\"id\": \"second\",\n\t\t}),\n\t\tNewRow(RowData{\n\t\t\t\"id\": \"third\",\n\t\t}),\n\t}).Focused(true)\n\n\tassert.False(t, model.GetVisibleRows()[0].selected, \"Row shouldn't be selected to start\")\n\n\tmodel, _ = model.Update(tea.KeyMsg{Type: tea.KeyEnter})\n\n\tassert.False(t, model.GetVisibleRows()[0].selected, \"Row shouldn't be selected after key press\")\n}\n\nfunc TestSelectingRowToggles(t *testing.T) {\n\tcols := []Column{\n\t\tNewColumn(\"id\", \"ID\", 3),\n\t}\n\n\tmodel := New(cols).WithRows([]Row{\n\t\tNewRow(RowData{\n\t\t\t\"id\": \"first\",\n\t\t}),\n\t\tNewRow(RowData{\n\t\t\t\"id\": \"second\",\n\t\t}),\n\t\tNewRow(RowData{\n\t\t\t\"id\": \"third\",\n\t\t}),\n\t}).Focused(true).SelectableRows(true)\n\n\tkeyEnter := tea.KeyMsg{Type: tea.KeyEnter}\n\tkeyDown := tea.KeyMsg{Type: tea.KeyDown}\n\n\tassert.False(t, model.GetVisibleRows()[0].selected, \"Row shouldn't be selected to start\")\n\tassert.Len(t, model.SelectedRows(), 0)\n\n\tmodel, _ = model.Update(keyEnter)\n\tassert.True(t, model.GetVisibleRows()[0].selected, \"Row should be selected after first toggle\")\n\tassert.Len(t, model.SelectedRows(), 1)\n\n\tmodel, _ = model.Update(keyEnter)\n\tassert.False(t, model.GetVisibleRows()[0].selected, \"Row should not be selected after second toggle\")\n\tassert.Len(t, model.SelectedRows(), 0)\n\n\tmodel, _ = model.Update(keyDown)\n\tmodel, _ = model.Update(keyEnter)\n\tassert.True(t, model.GetVisibleRows()[1].selected, \"Second row should be selected after moving and toggling\")\n}\n\nfunc TestFilterWithKeypresses(t *testing.T) {\n\tcols := []Column{\n\t\tNewColumn(\"name\", \"Name\", 10).WithFiltered(true),\n\t}\n\n\tmodel := New(cols).WithRows([]Row{\n\t\tNewRow(RowData{\"name\": \"Pikachu\"}),\n\t\tNewRow(RowData{\"name\": \"Charmander\"}),\n\t}).Focused(true).Filtered(true)\n\n\thitKey := func(key rune) {\n\t\tmodel, _ = model.Update(tea.KeyMsg{\n\t\t\tType:  tea.KeyRunes,\n\t\t\tRunes: []rune{key},\n\t\t})\n\t}\n\n\thitEnter := func() {\n\t\tmodel, _ = model.Update(tea.KeyMsg{Type: tea.KeyEnter})\n\t}\n\n\thitEscape := func() {\n\t\tmodel, _ = model.Update(tea.KeyMsg{Type: tea.KeyEscape})\n\t}\n\n\tvisible := model.GetVisibleRows()\n\n\tassert.Len(t, visible, 2)\n\thitKey(rune(model.KeyMap().Filter.Keys()[0][0]))\n\tassert.Len(t, visible, 2)\n\thitKey('p')\n\thitKey('i')\n\thitKey('k')\n\n\tvisible = model.GetVisibleRows()\n\n\tassert.Len(t, visible, 1)\n\n\thitEnter()\n\n\thitKey('x')\n\n\tvisible = model.GetVisibleRows()\n\n\tassert.Len(t, visible, 1)\n\n\thitEscape()\n\n\tvisible = model.GetVisibleRows()\n\n\tassert.Len(t, visible, 2)\n}\n\n// This is a long test with a lot of movement keys pressed, that's okay because\n// it's simply repetitive and tracking the same kind of state change many times\n//\n//nolint:funlen\nfunc TestSelectOnFilteredTableDoesntLoseRows(t *testing.T) {\n\t// Issue: https://github.com/Evertras/bubble-table/issues/170\n\t//\n\t// Basically, if you filter a table and then select a row, then\n\t// clear the filter, then all the other rows should still exist.\n\n\tcols := []Column{\n\t\tNewColumn(\"name\", \"Name\", 10).WithFiltered(true),\n\t}\n\n\tmodel := New(cols).WithRows([]Row{\n\t\tNewRow(RowData{\"name\": \"Charmander\"}),\n\t\tNewRow(RowData{\"name\": \"Pikachu\"}),\n\t}).Focused(true).Filtered(true).SelectableRows(true)\n\n\thitKey := func(key rune) {\n\t\tmodel, _ = model.Update(tea.KeyMsg{\n\t\t\tType:  tea.KeyRunes,\n\t\t\tRunes: []rune{key},\n\t\t})\n\t}\n\n\thitEnter := func() {\n\t\tmodel, _ = model.Update(tea.KeyMsg{Type: tea.KeyEnter})\n\t}\n\n\thitEscape := func() {\n\t\tmodel, _ = model.Update(tea.KeyMsg{Type: tea.KeyEscape})\n\t}\n\n\thitSpacebar := func() {\n\t\tmodel, _ = model.Update(tea.KeyMsg{Type: tea.KeySpace})\n\t}\n\n\t// First, apply the filter\n\t//\n\t// Note that we try and filter for the second row, \"Pikachu\"\n\t// so that we can better ensure everything is stably intact\n\tvisible := model.GetVisibleRows()\n\n\tassert.Len(t, visible, 2)\n\thitKey(rune(model.KeyMap().Filter.Keys()[0][0]))\n\tassert.Len(t, visible, 2)\n\thitKey('p')\n\thitKey('i')\n\thitKey('k')\n\n\tvisible = model.GetVisibleRows()\n\n\tassert.Len(t, visible, 1)\n\n\thitEnter()\n\n\t// Now apply the selection toggle\n\thitSpacebar()\n\n\tvisible = model.GetVisibleRows()\n\tassert.Len(t, visible, 1)\n\tassert.True(t, visible[0].selected)\n\n\t// Now clear the filter and make sure everything is intact\n\thitEscape()\n\n\tvisible = model.GetVisibleRows()\n\n\tassert.Len(t, visible, 2)\n\n\tif t.Failed() {\n\t\treturn\n\t}\n\n\tassert.False(t, visible[0].selected)\n\tassert.True(t, visible[1].selected)\n}\n"
  },
  {
    "path": "table/view.go",
    "content": "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 in a newline, so that it can be\n// composed with other elements more consistently.\n//\n//nolint:cyclop\nfunc (m Model) View() string {\n\t// Safety valve for empty tables\n\tif len(m.columns) == 0 {\n\t\treturn \"\"\n\t}\n\n\tbody := strings.Builder{}\n\n\trowStrs := make([]string, 0, 1)\n\n\theaders := m.renderHeaders()\n\n\tstartRowIndex, endRowIndex := m.VisibleIndices()\n\tnumRows := endRowIndex - startRowIndex + 1\n\n\tpadding := m.calculatePadding(numRows)\n\n\tif m.headerVisible {\n\t\trowStrs = append(rowStrs, headers)\n\t} else if numRows > 0 || padding > 0 {\n\t\t//nolint: mnd // This is just getting the first newlined substring\n\t\tsplit := strings.SplitN(headers, \"\\n\", 2)\n\t\trowStrs = append(rowStrs, split[0])\n\t}\n\n\tfor i := startRowIndex; i <= endRowIndex; i++ {\n\t\trowStrs = append(rowStrs, m.renderRow(i, padding == 0 && i == endRowIndex))\n\t}\n\n\tfor i := 1; i <= padding; i++ {\n\t\trowStrs = append(rowStrs, m.renderBlankRow(i == padding))\n\t}\n\n\tvar footer string\n\n\tif len(rowStrs) > 0 {\n\t\tfooter = m.renderFooter(lipgloss.Width(rowStrs[0]), false)\n\t} else {\n\t\tfooter = m.renderFooter(lipgloss.Width(headers), true)\n\t}\n\n\tif footer != \"\" {\n\t\trowStrs = append(rowStrs, footer)\n\t}\n\n\tif len(rowStrs) == 0 {\n\t\treturn \"\"\n\t}\n\n\tbody.WriteString(lipgloss.JoinVertical(lipgloss.Left, rowStrs...))\n\n\treturn body.String()\n}\n"
  },
  {
    "path": "table/view_selectable_test.go",
    "content": "package table\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestSimple3x3WithSelectableDefaults(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"1\", \"1\", 4),\n\t\tNewColumn(\"2\", \"2\", 4),\n\t\tNewColumn(\"3\", \"3\", 4),\n\t})\n\n\trows := []Row{}\n\n\tfor rowIndex := 1; rowIndex <= 3; rowIndex++ {\n\t\trowData := RowData{}\n\n\t\tfor columnIndex := 1; columnIndex <= 3; columnIndex++ {\n\t\t\tid := fmt.Sprintf(\"%d\", columnIndex)\n\n\t\t\trowData[id] = fmt.Sprintf(\"%d,%d\", columnIndex, rowIndex)\n\t\t}\n\n\t\trows = append(rows, NewRow(rowData))\n\t}\n\n\tmodel = model.WithRows(rows).SelectableRows(true)\n\n\tconst expectedTable = `┏━━━┳━━━━┳━━━━┳━━━━┓\n┃[x]┃   1┃   2┃   3┃\n┣━━━╋━━━━╋━━━━╋━━━━┫\n┃[ ]┃ 1,1┃ 2,1┃ 3,1┃\n┃[ ]┃ 1,2┃ 2,2┃ 3,2┃\n┃[ ]┃ 1,3┃ 2,3┃ 3,3┃\n┗━━━┻━━━━┻━━━━┻━━━━┛`\n\n\trendered := model.View()\n\n\tassert.Equal(t, expectedTable, rendered)\n}\n\nfunc TestSimple3x3WithCustomSelectableText(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"1\", \"1\", 4),\n\t\tNewColumn(\"2\", \"2\", 4),\n\t\tNewColumn(\"3\", \"3\", 4),\n\t})\n\n\trows := []Row{}\n\n\tfor rowIndex := 1; rowIndex <= 3; rowIndex++ {\n\t\trowData := RowData{}\n\n\t\tfor columnIndex := 1; columnIndex <= 3; columnIndex++ {\n\t\t\tid := fmt.Sprintf(\"%d\", columnIndex)\n\n\t\t\trowData[id] = fmt.Sprintf(\"%d,%d\", columnIndex, rowIndex)\n\t\t}\n\n\t\trows = append(rows, NewRow(rowData))\n\t}\n\n\tmodel = model.WithRows(rows).\n\t\tSelectableRows(true).\n\t\tWithSelectedText(\" \", \"✓\")\n\n\tconst expectedTable = `┏━┳━━━━┳━━━━┳━━━━┓\n┃✓┃   1┃   2┃   3┃\n┣━╋━━━━╋━━━━╋━━━━┫\n┃ ┃ 1,1┃ 2,1┃ 3,1┃\n┃ ┃ 1,2┃ 2,2┃ 3,2┃\n┃ ┃ 1,3┃ 2,3┃ 3,3┃\n┗━┻━━━━┻━━━━┻━━━━┛`\n\n\trendered := model.View()\n\n\tassert.Equal(t, expectedTable, rendered)\n}\n\nfunc TestSimple3x3WithCustomSelectableTextAndFooter(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"1\", \"1\", 4),\n\t\tNewColumn(\"2\", \"2\", 4),\n\t\tNewColumn(\"3\", \"3\", 4),\n\t})\n\n\trows := []Row{}\n\n\tfor rowIndex := 1; rowIndex <= 3; rowIndex++ {\n\t\trowData := RowData{}\n\n\t\tfor columnIndex := 1; columnIndex <= 3; columnIndex++ {\n\t\t\tid := fmt.Sprintf(\"%d\", columnIndex)\n\n\t\t\trowData[id] = fmt.Sprintf(\"%d,%d\", columnIndex, rowIndex)\n\t\t}\n\n\t\trows = append(rows, NewRow(rowData))\n\t}\n\n\tmodel = model.WithRows(rows).\n\t\tSelectableRows(true).\n\t\tWithSelectedText(\" \", \"✓\").\n\t\tWithStaticFooter(\"Footer\")\n\n\tconst expectedTable = `┏━┳━━━━┳━━━━┳━━━━┓\n┃✓┃   1┃   2┃   3┃\n┣━╋━━━━╋━━━━╋━━━━┫\n┃ ┃ 1,1┃ 2,1┃ 3,1┃\n┃ ┃ 1,2┃ 2,2┃ 3,2┃\n┃ ┃ 1,3┃ 2,3┃ 3,3┃\n┣━┻━━━━┻━━━━┻━━━━┫\n┃          Footer┃\n┗━━━━━━━━━━━━━━━━┛`\n\n\trendered := model.View()\n\n\tassert.Equal(t, expectedTable, rendered)\n}\n\nfunc TestRegeneratingColumnsKeepsSelectableText(t *testing.T) {\n\tcolumns := []Column{\n\t\tNewColumn(\"1\", \"1\", 4),\n\t\tNewColumn(\"2\", \"2\", 4),\n\t\tNewColumn(\"3\", \"3\", 4),\n\t}\n\n\tmodel := New(columns)\n\n\trows := []Row{}\n\n\tfor rowIndex := 1; rowIndex <= 3; rowIndex++ {\n\t\trowData := RowData{}\n\n\t\tfor columnIndex := 1; columnIndex <= 3; columnIndex++ {\n\t\t\tid := fmt.Sprintf(\"%d\", columnIndex)\n\n\t\t\trowData[id] = fmt.Sprintf(\"%d,%d\", columnIndex, rowIndex)\n\t\t}\n\n\t\trows = append(rows, NewRow(rowData))\n\t}\n\n\tmodel = model.WithRows(rows).\n\t\tSelectableRows(true).\n\t\tWithSelectedText(\" \", \"✓\").\n\t\tWithColumns(columns)\n\n\tconst expectedTable = `┏━┳━━━━┳━━━━┳━━━━┓\n┃✓┃   1┃   2┃   3┃\n┣━╋━━━━╋━━━━╋━━━━┫\n┃ ┃ 1,1┃ 2,1┃ 3,1┃\n┃ ┃ 1,2┃ 2,2┃ 3,2┃\n┃ ┃ 1,3┃ 2,3┃ 3,3┃\n┗━┻━━━━┻━━━━┻━━━━┛`\n\n\trendered := model.View()\n\n\tassert.Equal(t, expectedTable, rendered)\n}\n"
  },
  {
    "path": "table/view_test.go",
    "content": "package table\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/charmbracelet/lipgloss\"\n\t\"github.com/mattn/go-runewidth\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestBasicTableShowsAllHeaders(t *testing.T) {\n\tconst (\n\t\tfirstKey   = \"first-key\"\n\t\tfirstTitle = \"First Title\"\n\t\tfirstWidth = 13\n\n\t\tsecondKey   = \"second-key\"\n\t\tsecondTitle = \"Second Title\"\n\t\tsecondWidth = 20\n\t)\n\n\tcolumns := []Column{\n\t\tNewColumn(firstKey, firstTitle, firstWidth),\n\t\tNewColumn(secondKey, secondTitle, secondWidth),\n\t}\n\n\tmodel := New(columns)\n\n\trendered := model.View()\n\n\tassert.Contains(t, rendered, firstTitle)\n\tassert.Contains(t, rendered, secondTitle)\n\n\tassert.False(t, strings.HasSuffix(rendered, \"\\n\"), \"Should not end in newline\")\n}\n\nfunc TestBasicTableTruncatesLongHeaders(t *testing.T) {\n\tconst (\n\t\tfirstKey   = \"first-key\"\n\t\tfirstTitle = \"First Title\"\n\t\tfirstWidth = 3\n\n\t\tsecondKey   = \"second-key\"\n\t\tsecondTitle = \"Second Title\"\n\t\tsecondWidth = 3\n\t)\n\n\tcolumns := []Column{\n\t\tNewColumn(firstKey, firstTitle, firstWidth),\n\t\tNewColumn(secondKey, secondTitle, secondWidth),\n\t}\n\n\tmodel := New(columns)\n\n\trendered := model.View()\n\n\tassert.Contains(t, rendered, \"Fi…\")\n\tassert.Contains(t, rendered, \"Se…\")\n\n\tassert.False(t, strings.HasSuffix(rendered, \"\\n\"), \"Should not end in newline\")\n}\n\nfunc TestNilColumnsSafelyReturnsEmptyView(t *testing.T) {\n\tmodel := New(nil)\n\n\tassert.Equal(t, \"\", model.View())\n}\n\nfunc TestSingleCellView(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"id\", \"ID\", 4),\n\t})\n\n\tconst expectedTable = `┏━━━━┓\n┃  ID┃\n┗━━━━┛`\n\n\trendered := model.View()\n\n\tassert.Equal(t, expectedTable, rendered)\n}\n\nfunc TestSingleColumnView(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"id\", \"ID\", 4),\n\t}).WithRows([]Row{\n\t\tNewRow(RowData{\"id\": \"1\"}),\n\t\tNewRow(RowData{\"id\": \"2\"}),\n\t})\n\n\tconst expectedTable = `┏━━━━┓\n┃  ID┃\n┣━━━━┫\n┃   1┃\n┃   2┃\n┗━━━━┛`\n\n\trendered := model.View()\n\n\tassert.Equal(t, expectedTable, rendered)\n}\n\nfunc TestSingleColumnViewSorted(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"id\", \"ID\", 4),\n\t}).WithRows([]Row{\n\t\tNewRow(RowData{\"id\": \"1\"}),\n\t\tNewRow(RowData{\"id\": \"2\"}),\n\t}).SortByDesc(\"id\")\n\n\tconst expectedTable = `┏━━━━┓\n┃  ID┃\n┣━━━━┫\n┃   2┃\n┃   1┃\n┗━━━━┛`\n\n\trendered := model.View()\n\n\tassert.Equal(t, expectedTable, rendered)\n}\n\nfunc TestSingleRowView(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"1\", \"1\", 4),\n\t\tNewColumn(\"2\", \"2\", 4),\n\t\tNewColumn(\"3\", \"3\", 4),\n\t})\n\n\tconst expectedTable = `┏━━━━┳━━━━┳━━━━┓\n┃   1┃   2┃   3┃\n┗━━━━┻━━━━┻━━━━┛`\n\n\trendered := model.View()\n\n\tassert.Equal(t, expectedTable, rendered)\n}\n\nfunc TestSingleRowViewWithHiddenHeader(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"1\", \"1\", 4),\n\t\tNewColumn(\"2\", \"2\", 4),\n\t\tNewColumn(\"3\", \"3\", 4),\n\t}).\n\t\tWithHeaderVisibility(false).\n\t\tWithRows([]Row{\n\t\t\tNewRow(RowData{\"1\": \"a\", \"2\": \"b\", \"3\": \"c\"}),\n\t\t})\n\n\tconst expectedTable = `┏━━━━┳━━━━┳━━━━┓\n┃   a┃   b┃   c┃\n┗━━━━┻━━━━┻━━━━┛`\n\n\trendered := model.View()\n\n\tassert.Equal(t, expectedTable, rendered)\n}\n\nfunc TestTableWithNoRowsAndHiddenHeaderHidesTable(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"1\", \"1\", 4),\n\t\tNewColumn(\"2\", \"2\", 4),\n\t\tNewColumn(\"3\", \"3\", 4),\n\t}).WithHeaderVisibility(false)\n\n\tconst expectedTable = \"\"\n\n\trendered := model.View()\n\n\tassert.Equal(t, expectedTable, rendered)\n}\n\nfunc TestSimple3x3(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"1\", \"1\", 4),\n\t\tNewColumn(\"2\", \"2\", 4),\n\t\tNewColumn(\"3\", \"3\", 4),\n\t})\n\n\trows := []Row{}\n\n\tfor rowIndex := 1; rowIndex <= 3; rowIndex++ {\n\t\trowData := RowData{}\n\n\t\tfor columnIndex := 1; columnIndex <= 3; columnIndex++ {\n\t\t\tid := fmt.Sprintf(\"%d\", columnIndex)\n\n\t\t\trowData[id] = fmt.Sprintf(\"%d,%d\", columnIndex, rowIndex)\n\t\t}\n\n\t\trows = append(rows, NewRow(rowData))\n\t}\n\n\tmodel = model.WithRows(rows)\n\n\tconst expectedTable = `┏━━━━┳━━━━┳━━━━┓\n┃   1┃   2┃   3┃\n┣━━━━╋━━━━╋━━━━┫\n┃ 1,1┃ 2,1┃ 3,1┃\n┃ 1,2┃ 2,2┃ 3,2┃\n┃ 1,3┃ 2,3┃ 3,3┃\n┗━━━━┻━━━━┻━━━━┛`\n\n\trendered := model.View()\n\n\tassert.Equal(t, expectedTable, rendered)\n}\n\nfunc TestSimple3x3WithHiddenHeader(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"1\", \"1\", 4),\n\t\tNewColumn(\"2\", \"2\", 4),\n\t\tNewColumn(\"3\", \"3\", 4),\n\t}).WithHeaderVisibility(false)\n\n\trows := []Row{}\n\n\tfor rowIndex := 1; rowIndex <= 3; rowIndex++ {\n\t\trowData := RowData{}\n\n\t\tfor columnIndex := 1; columnIndex <= 3; columnIndex++ {\n\t\t\tid := fmt.Sprintf(\"%d\", columnIndex)\n\n\t\t\trowData[id] = fmt.Sprintf(\"%d,%d\", columnIndex, rowIndex)\n\t\t}\n\n\t\trows = append(rows, NewRow(rowData))\n\t}\n\n\tmodel = model.WithRows(rows)\n\n\tconst expectedTable = `┏━━━━┳━━━━┳━━━━┓\n┃ 1,1┃ 2,1┃ 3,1┃\n┃ 1,2┃ 2,2┃ 3,2┃\n┃ 1,3┃ 2,3┃ 3,3┃\n┗━━━━┻━━━━┻━━━━┛`\n\n\trendered := model.View()\n\n\tassert.Equal(t, expectedTable, rendered)\n}\n\nfunc TestSingleHeaderWithFooter(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"id\", \"ID\", 4),\n\t}).WithStaticFooter(\"Foot\")\n\n\tconst expectedTable = `┏━━━━┓\n┃  ID┃\n┣━━━━┫\n┃Foot┃\n┗━━━━┛`\n\trendered := model.View()\n\n\tassert.Equal(t, expectedTable, rendered)\n}\n\nfunc TestSingleColumnWithFooterAndHiddenHeader(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"id\", \"ID\", 4),\n\t}).\n\t\tWithStaticFooter(\"Foot\").\n\t\tWithHeaderVisibility(false)\n\n\tconst expectedTable = `┏━━━━┓\n┃Foot┃\n┗━━━━┛`\n\trendered := model.View()\n\n\tassert.Equal(t, expectedTable, rendered)\n}\n\nfunc TestSingleRowWithFooterView(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"1\", \"1\", 4),\n\t\tNewColumn(\"2\", \"2\", 4),\n\t\tNewColumn(\"3\", \"3\", 4),\n\t}).WithStaticFooter(\"Footer\")\n\n\tconst expectedTable = `┏━━━━┳━━━━┳━━━━┓\n┃   1┃   2┃   3┃\n┣━━━━┻━━━━┻━━━━┫\n┃        Footer┃\n┗━━━━━━━━━━━━━━┛`\n\n\trendered := model.View()\n\n\tassert.Equal(t, expectedTable, rendered)\n}\n\nfunc TestSingleRowWithFooterViewAndBaseStyle(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"1\", \"1\", 4),\n\t\tNewColumn(\"2\", \"2\", 4),\n\t\tNewColumn(\"3\", \"3\", 4),\n\t}).WithStaticFooter(\"Footer\").WithBaseStyle(lipgloss.NewStyle().Align(lipgloss.Left))\n\n\tconst expectedTable = `┏━━━━┳━━━━┳━━━━┓\n┃1   ┃2   ┃3   ┃\n┣━━━━┻━━━━┻━━━━┫\n┃Footer        ┃\n┗━━━━━━━━━━━━━━┛`\n\n\trendered := model.View()\n\n\tassert.Equal(t, expectedTable, rendered)\n}\n\nfunc TestSingleRowWithFooterViewAndBaseStyleWithHiddenHeader(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"1\", \"1\", 4),\n\t\tNewColumn(\"2\", \"2\", 4),\n\t\tNewColumn(\"3\", \"3\", 4),\n\t}).\n\t\tWithStaticFooter(\"Footer\").\n\t\tWithBaseStyle(lipgloss.NewStyle().Align(lipgloss.Left)).\n\t\tWithHeaderVisibility(false)\n\n\tconst expectedTable = `┏━━━━━━━━━━━━━━┓\n┃Footer        ┃\n┗━━━━━━━━━━━━━━┛`\n\n\trendered := model.View()\n\n\tassert.Equal(t, expectedTable, rendered)\n}\n\nfunc TestSingleColumnWithFooterView(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"id\", \"ID\", 4),\n\t}).WithRows([]Row{\n\t\tNewRow(RowData{\"id\": \"1\"}),\n\t\tNewRow(RowData{\"id\": \"2\"}),\n\t}).WithStaticFooter(\"Foot\")\n\n\tconst expectedTable = `┏━━━━┓\n┃  ID┃\n┣━━━━┫\n┃   1┃\n┃   2┃\n┣━━━━┫\n┃Foot┃\n┗━━━━┛`\n\n\trendered := model.View()\n\n\tassert.Equal(t, expectedTable, rendered)\n}\n\nfunc TestSingleColumnWithFooterViewAndHiddenHeader(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"id\", \"ID\", 4),\n\t}).\n\t\tWithRows([]Row{\n\t\t\tNewRow(RowData{\"id\": \"1\"}),\n\t\t\tNewRow(RowData{\"id\": \"2\"}),\n\t\t}).\n\t\tWithStaticFooter(\"Foot\").\n\t\tWithHeaderVisibility(false)\n\n\tconst expectedTable = `┏━━━━┓\n┃   1┃\n┃   2┃\n┣━━━━┫\n┃Foot┃\n┗━━━━┛`\n\n\trendered := model.View()\n\n\tassert.Equal(t, expectedTable, rendered)\n}\n\nfunc TestSimple3x3WithFooterView(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"1\", \"1\", 4),\n\t\tNewColumn(\"2\", \"2\", 4),\n\t\tNewColumn(\"3\", \"3\", 4),\n\t})\n\n\trows := []Row{}\n\n\tfor rowIndex := 1; rowIndex <= 3; rowIndex++ {\n\t\trowData := RowData{}\n\n\t\tfor columnIndex := 1; columnIndex <= 3; columnIndex++ {\n\t\t\tid := fmt.Sprintf(\"%d\", columnIndex)\n\n\t\t\trowData[id] = fmt.Sprintf(\"%d,%d\", columnIndex, rowIndex)\n\t\t}\n\n\t\trows = append(rows, NewRow(rowData))\n\t}\n\n\tmodel = model.WithRows(rows).WithStaticFooter(\"Footer\")\n\n\tconst expectedTable = `┏━━━━┳━━━━┳━━━━┓\n┃   1┃   2┃   3┃\n┣━━━━╋━━━━╋━━━━┫\n┃ 1,1┃ 2,1┃ 3,1┃\n┃ 1,2┃ 2,2┃ 3,2┃\n┃ 1,3┃ 2,3┃ 3,3┃\n┣━━━━┻━━━━┻━━━━┫\n┃        Footer┃\n┗━━━━━━━━━━━━━━┛`\n\n\trendered := model.View()\n\n\tassert.Equal(t, expectedTable, rendered)\n}\n\nfunc TestSimple3x3WithMissingData(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"1\", \"1\", 4),\n\t\tNewColumn(\"2\", \"2\", 4),\n\t\tNewColumn(\"3\", \"3\", 4),\n\t})\n\n\trows := []Row{}\n\n\tfor rowIndex := 1; rowIndex <= 3; rowIndex++ {\n\t\trowData := RowData{}\n\n\t\tfor columnIndex := 1; columnIndex <= 3; columnIndex++ {\n\t\t\t// Take out the center\n\t\t\tif rowIndex == 2 && columnIndex == 2 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tid := fmt.Sprintf(\"%d\", columnIndex)\n\n\t\t\trowData[id] = fmt.Sprintf(\"%d,%d\", columnIndex, rowIndex)\n\t\t}\n\n\t\trows = append(rows, NewRow(rowData))\n\t}\n\n\tmodel = model.WithRows(rows).WithStaticFooter(\"Footer\")\n\n\tconst expectedTable = `┏━━━━┳━━━━┳━━━━┓\n┃   1┃   2┃   3┃\n┣━━━━╋━━━━╋━━━━┫\n┃ 1,1┃ 2,1┃ 3,1┃\n┃ 1,2┃    ┃ 3,2┃\n┃ 1,3┃ 2,3┃ 3,3┃\n┣━━━━┻━━━━┻━━━━┫\n┃        Footer┃\n┗━━━━━━━━━━━━━━┛`\n\n\trendered := model.View()\n\n\tassert.Equal(t, expectedTable, rendered)\n}\n\nfunc TestFmtStringWithMissingData(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"1\", \"1\", 4),\n\t\tNewColumn(\"2\", \"2\", 4).WithFormatString(\"%.1f\"),\n\t\tNewColumn(\"3\", \"3\", 4),\n\t})\n\n\trows := []Row{}\n\n\tfor rowIndex := 1; rowIndex <= 3; rowIndex++ {\n\t\trowData := RowData{}\n\n\t\tfor columnIndex := 1; columnIndex <= 3; columnIndex++ {\n\t\t\t// Take out the center\n\t\t\tif rowIndex == 2 && columnIndex == 2 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tid := fmt.Sprintf(\"%d\", columnIndex)\n\n\t\t\trowData[id] = float64(columnIndex) + float64(rowIndex)/10.0\n\t\t}\n\n\t\trows = append(rows, NewRow(rowData))\n\t}\n\n\tmodel = model.WithRows(rows).WithStaticFooter(\"Footer\")\n\n\tconst expectedTable = `┏━━━━┳━━━━┳━━━━┓\n┃   1┃   2┃   3┃\n┣━━━━╋━━━━╋━━━━┫\n┃ 1.1┃ 2.1┃ 3.1┃\n┃ 1.2┃    ┃ 3.2┃\n┃ 1.3┃ 2.3┃ 3.3┃\n┣━━━━┻━━━━┻━━━━┫\n┃        Footer┃\n┗━━━━━━━━━━━━━━┛`\n\n\trendered := model.View()\n\n\tassert.Equal(t, expectedTable, rendered)\n}\n\nfunc TestSimple3x3WithMissingIndicator(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"1\", \"1\", 4),\n\t\tNewColumn(\"2\", \"2\", 4),\n\t\tNewColumn(\"3\", \"3\", 4),\n\t}).WithMissingDataIndicator(\"XX\")\n\n\trows := []Row{}\n\n\tfor rowIndex := 1; rowIndex <= 3; rowIndex++ {\n\t\trowData := RowData{}\n\n\t\tfor columnIndex := 1; columnIndex <= 3; columnIndex++ {\n\t\t\t// Take out the center\n\t\t\tif rowIndex == 2 && columnIndex == 2 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcolumnKey := fmt.Sprintf(\"%d\", columnIndex)\n\n\t\t\tif rowIndex == 2 && columnIndex == 3 {\n\t\t\t\t// Empty string to ensure that zero value data is not 'missing'\n\t\t\t\trowData[columnKey] = \"\"\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\trowData[columnKey] = fmt.Sprintf(\"%d,%d\", columnIndex, rowIndex)\n\t\t}\n\n\t\trows = append(rows, NewRow(rowData))\n\t}\n\n\tmodel = model.WithRows(rows).WithStaticFooter(\"Footer\")\n\n\tconst expectedTable = `┏━━━━┳━━━━┳━━━━┓\n┃   1┃   2┃   3┃\n┣━━━━╋━━━━╋━━━━┫\n┃ 1,1┃ 2,1┃ 3,1┃\n┃ 1,2┃  XX┃    ┃\n┃ 1,3┃ 2,3┃ 3,3┃\n┣━━━━┻━━━━┻━━━━┫\n┃        Footer┃\n┗━━━━━━━━━━━━━━┛`\n\n\trendered := model.View()\n\n\tassert.Equal(t, expectedTable, rendered)\n}\n\nfunc TestFmtStringWithMissingIndicator(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"1\", \"1\", 4),\n\t\tNewColumn(\"2\", \"2\", 4).WithFormatString(\"%.1f\"),\n\t\tNewColumn(\"3\", \"3\", 4),\n\t}).WithMissingDataIndicator(\"XX\")\n\n\trows := []Row{}\n\n\tfor rowIndex := 1; rowIndex <= 3; rowIndex++ {\n\t\trowData := RowData{}\n\n\t\tfor columnIndex := 1; columnIndex <= 3; columnIndex++ {\n\t\t\t// Take out the center\n\t\t\tif rowIndex == 2 && columnIndex == 2 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tid := fmt.Sprintf(\"%d\", columnIndex)\n\n\t\t\trowData[id] = float64(columnIndex) + float64(rowIndex)/10.0\n\t\t}\n\n\t\trows = append(rows, NewRow(rowData))\n\t}\n\n\tmodel = model.WithRows(rows).WithStaticFooter(\"Footer\")\n\n\tconst expectedTable = `┏━━━━┳━━━━┳━━━━┓\n┃   1┃   2┃   3┃\n┣━━━━╋━━━━╋━━━━┫\n┃ 1.1┃ 2.1┃ 3.1┃\n┃ 1.2┃  XX┃ 3.2┃\n┃ 1.3┃ 2.3┃ 3.3┃\n┣━━━━┻━━━━┻━━━━┫\n┃        Footer┃\n┗━━━━━━━━━━━━━━┛`\n\n\trendered := model.View()\n\n\tassert.Equal(t, expectedTable, rendered)\n}\n\nfunc TestSimple3x3WithMissingIndicatorStyled(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"1\", \"1\", 4),\n\t\tNewColumn(\"2\", \"2\", 4),\n\t\tNewColumn(\"3\", \"3\", 4),\n\t}).WithMissingDataIndicatorStyled(StyledCell{\n\t\tStyle: lipgloss.NewStyle().Align(lipgloss.Left),\n\t\tData:  \"XX\",\n\t})\n\n\trows := []Row{}\n\n\tfor rowIndex := 1; rowIndex <= 3; rowIndex++ {\n\t\trowData := RowData{}\n\n\t\tfor columnIndex := 1; columnIndex <= 3; columnIndex++ {\n\t\t\t// Take out the center\n\t\t\tif rowIndex == 2 && columnIndex == 2 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tid := fmt.Sprintf(\"%d\", columnIndex)\n\n\t\t\trowData[id] = fmt.Sprintf(\"%d,%d\", columnIndex, rowIndex)\n\t\t}\n\n\t\trows = append(rows, NewRow(rowData))\n\t}\n\n\tmodel = model.WithRows(rows).WithStaticFooter(\"Footer\")\n\n\tconst expectedTable = `┏━━━━┳━━━━┳━━━━┓\n┃   1┃   2┃   3┃\n┣━━━━╋━━━━╋━━━━┫\n┃ 1,1┃ 2,1┃ 3,1┃\n┃ 1,2┃XX  ┃ 3,2┃\n┃ 1,3┃ 2,3┃ 3,3┃\n┣━━━━┻━━━━┻━━━━┫\n┃        Footer┃\n┗━━━━━━━━━━━━━━┛`\n\n\trendered := model.View()\n\n\tassert.Equal(t, expectedTable, rendered)\n}\n\nfunc TestFmtStringWithMissingIndicatorStyled(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"1\", \"1\", 4),\n\t\tNewColumn(\"2\", \"2\", 4).WithFormatString(\"%.1f\"),\n\t\tNewColumn(\"3\", \"3\", 4),\n\t}).WithMissingDataIndicatorStyled(StyledCell{\n\t\tStyle: lipgloss.NewStyle().Align(lipgloss.Left),\n\t\tData:  \"XX\",\n\t})\n\n\trows := []Row{}\n\n\tfor rowIndex := 1; rowIndex <= 3; rowIndex++ {\n\t\trowData := RowData{}\n\n\t\tfor columnIndex := 1; columnIndex <= 3; columnIndex++ {\n\t\t\t// Take out the center\n\t\t\tif rowIndex == 2 && columnIndex == 2 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tid := fmt.Sprintf(\"%d\", columnIndex)\n\n\t\t\trowData[id] = float64(columnIndex) + float64(rowIndex)/10.0\n\t\t}\n\n\t\trows = append(rows, NewRow(rowData))\n\t}\n\n\tmodel = model.WithRows(rows).WithStaticFooter(\"Footer\")\n\n\tconst expectedTable = `┏━━━━┳━━━━┳━━━━┓\n┃   1┃   2┃   3┃\n┣━━━━╋━━━━╋━━━━┫\n┃ 1.1┃ 2.1┃ 3.1┃\n┃ 1.2┃XX  ┃ 3.2┃\n┃ 1.3┃ 2.3┃ 3.3┃\n┣━━━━┻━━━━┻━━━━┫\n┃        Footer┃\n┗━━━━━━━━━━━━━━┛`\n\n\trendered := model.View()\n\n\tassert.Equal(t, expectedTable, rendered)\n}\n\nfunc TestPaged3x3WithNoSpecifiedFooter(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"1\", \"1\", 4),\n\t\tNewColumn(\"2\", \"2\", 4),\n\t\tNewColumn(\"3\", \"3\", 4),\n\t})\n\n\trows := []Row{}\n\n\tfor rowIndex := 1; rowIndex <= 3; rowIndex++ {\n\t\trowData := RowData{}\n\n\t\tfor columnIndex := 1; columnIndex <= 3; columnIndex++ {\n\t\t\tid := fmt.Sprintf(\"%d\", columnIndex)\n\n\t\t\trowData[id] = fmt.Sprintf(\"%d,%d\", columnIndex, rowIndex)\n\t\t}\n\n\t\trows = append(rows, NewRow(rowData))\n\t}\n\n\tmodel = model.WithRows(rows).WithPageSize(2)\n\n\tconst expectedTable = `┏━━━━┳━━━━┳━━━━┓\n┃   1┃   2┃   3┃\n┣━━━━╋━━━━╋━━━━┫\n┃ 1,1┃ 2,1┃ 3,1┃\n┃ 1,2┃ 2,2┃ 3,2┃\n┣━━━━┻━━━━┻━━━━┫\n┃           1/2┃\n┗━━━━━━━━━━━━━━┛`\n\n\trendered := model.View()\n\n\tassert.Equal(t, expectedTable, rendered)\n}\n\nfunc TestPaged3x3WithStaticFooter(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"1\", \"1\", 4),\n\t\tNewColumn(\"2\", \"2\", 4),\n\t\tNewColumn(\"3\", \"3\", 4),\n\t})\n\n\trows := []Row{}\n\n\tfor rowIndex := 1; rowIndex <= 3; rowIndex++ {\n\t\trowData := RowData{}\n\n\t\tfor columnIndex := 1; columnIndex <= 3; columnIndex++ {\n\t\t\tid := fmt.Sprintf(\"%d\", columnIndex)\n\n\t\t\trowData[id] = fmt.Sprintf(\"%d,%d\", columnIndex, rowIndex)\n\t\t}\n\n\t\trows = append(rows, NewRow(rowData))\n\t}\n\n\tmodel = model.WithRows(rows).WithPageSize(2).WithStaticFooter(\"Override\")\n\n\tconst expectedTable = `┏━━━━┳━━━━┳━━━━┓\n┃   1┃   2┃   3┃\n┣━━━━╋━━━━╋━━━━┫\n┃ 1,1┃ 2,1┃ 3,1┃\n┃ 1,2┃ 2,2┃ 3,2┃\n┣━━━━┻━━━━┻━━━━┫\n┃      Override┃\n┗━━━━━━━━━━━━━━┛`\n\n\trendered := model.View()\n\n\tassert.Equal(t, expectedTable, rendered)\n}\n\nfunc TestSimple3x3StyleOverridesAsBaseColumnRowCell(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"1\", \"1\", 6),\n\t\tNewColumn(\"2\", \"2\", 6).WithStyle(lipgloss.NewStyle().Align(lipgloss.Left)),\n\t\tNewColumn(\"3\", \"3\", 6),\n\t}).WithBaseStyle(lipgloss.NewStyle().Align(lipgloss.Center))\n\n\trows := []Row{}\n\n\tfor rowIndex := 1; rowIndex <= 3; rowIndex++ {\n\t\trowData := RowData{}\n\n\t\tfor columnIndex := 1; columnIndex <= 3; columnIndex++ {\n\t\t\tid := fmt.Sprintf(\"%d\", columnIndex)\n\n\t\t\trowData[id] = fmt.Sprintf(\"%d,%d\", columnIndex, rowIndex)\n\t\t}\n\n\t\trows = append(rows, NewRow(rowData))\n\t}\n\n\t// Test overrides with alignment because it's easy to check output string\n\trows[0] = rows[0].WithStyle(lipgloss.NewStyle().Align(lipgloss.Left))\n\trows[0].Data[\"2\"] = NewStyledCell(\"R\", lipgloss.NewStyle().Align(lipgloss.Right))\n\n\trows[2] = rows[2].WithStyle(lipgloss.NewStyle().Align(lipgloss.Right))\n\n\tmodel = model.WithRows(rows)\n\n\tconst expectedTable = `┏━━━━━━┳━━━━━━┳━━━━━━┓\n┃  1   ┃2     ┃  3   ┃\n┣━━━━━━╋━━━━━━╋━━━━━━┫\n┃1,1   ┃     R┃3,1   ┃\n┃ 1,2  ┃2,2   ┃ 3,2  ┃\n┃   1,3┃   2,3┃   3,3┃\n┗━━━━━━┻━━━━━━┻━━━━━━┛`\n\n\trendered := model.View()\n\n\tassert.Equal(t, expectedTable, rendered)\n}\n\nfunc TestSimple3x3CellStyleFuncOverridesAsBaseColumnRowCell(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"1\", \"1\", 6),\n\t\tNewColumn(\"2\", \"2\", 6).WithStyle(lipgloss.NewStyle().Align(lipgloss.Left)),\n\t\tNewColumn(\"3\", \"3\", 6),\n\t}).WithBaseStyle(lipgloss.NewStyle().Align(lipgloss.Center)).\n\t\tWithGlobalMetadata(map[string]any{\n\t\t\t\"testValue\": 37,\n\t\t})\n\n\trows := []Row{}\n\n\tfor rowIndex := 1; rowIndex <= 3; rowIndex++ {\n\t\trowData := RowData{}\n\n\t\tfor columnIndex := 1; columnIndex <= 3; columnIndex++ {\n\t\t\tid := fmt.Sprintf(\"%d\", columnIndex)\n\n\t\t\trowData[id] = fmt.Sprintf(\"%d,%d\", columnIndex, rowIndex)\n\t\t}\n\n\t\trows = append(rows, NewRow(rowData))\n\t}\n\n\t// Test overrides with alignment because it's easy to check output string\n\trows[0] = rows[0].WithStyle(lipgloss.NewStyle().Align(lipgloss.Left))\n\trows[0].Data[\"2\"] = NewStyledCellWithStyleFunc(\"R\", func(input StyledCellFuncInput) lipgloss.Style {\n\t\t// Do some checks to make sure we're given the right information as a bonus test\n\t\tassert.Equal(t, \"2\", input.Column.Key(), \"Wrong column key given to style func\")\n\t\tassert.Equal(t, \"R\", input.Data, \"Wrong data given to style func\")\n\t\tassert.Equal(t, \"1,1\", input.Row.Data[\"1\"], \"Wrong row given to style func\")\n\t\tassert.Equal(t, 37, input.GlobalMetadata[\"testValue\"], \"Wrong table metadata given to style func\")\n\n\t\treturn lipgloss.NewStyle().Align(lipgloss.Right)\n\t})\n\n\trows[2] = rows[2].WithStyle(lipgloss.NewStyle().Align(lipgloss.Right))\n\n\tmodel = model.WithRows(rows)\n\n\tconst expectedTable = `┏━━━━━━┳━━━━━━┳━━━━━━┓\n┃  1   ┃2     ┃  3   ┃\n┣━━━━━━╋━━━━━━╋━━━━━━┫\n┃1,1   ┃     R┃3,1   ┃\n┃ 1,2  ┃2,2   ┃ 3,2  ┃\n┃   1,3┃   2,3┃   3,3┃\n┗━━━━━━┻━━━━━━┻━━━━━━┛`\n\n\trendered := model.View()\n\n\tassert.Equal(t, expectedTable, rendered)\n}\n\nfunc TestRowStyleFuncAppliesAfterBaseStyleAndColStylesAndBeforeRowStyle(t *testing.T) {\n\tstyleFunc := func(input RowStyleFuncInput) lipgloss.Style {\n\t\tif input.Index%2 == 0 {\n\t\t\treturn lipgloss.NewStyle().Align(lipgloss.Left)\n\t\t}\n\n\t\treturn lipgloss.NewStyle()\n\t}\n\n\tmodel := New([]Column{\n\t\tNewColumn(\"1\", \"1\", 6),\n\t\t// This column style should be overridden by the style func\n\t\tNewColumn(\"2\", \"2\", 6).WithStyle(lipgloss.NewStyle().Align(lipgloss.Right)),\n\t\tNewColumn(\"3\", \"3\", 6),\n\t}).\n\t\tWithBaseStyle(lipgloss.NewStyle().Align(lipgloss.Center)).\n\t\tWithRowStyleFunc(styleFunc)\n\n\trows := []Row{}\n\n\tfor rowIndex := 1; rowIndex <= 5; rowIndex++ {\n\t\trowData := RowData{}\n\n\t\tfor columnIndex := 1; columnIndex <= 3; columnIndex++ {\n\t\t\tid := fmt.Sprintf(\"%d\", columnIndex)\n\n\t\t\trowData[id] = fmt.Sprintf(\"%d,%d\", columnIndex, rowIndex)\n\t\t}\n\n\t\trows = append(rows, NewRow(rowData))\n\t}\n\n\trows[0] = rows[0].WithStyle(lipgloss.NewStyle().Align(lipgloss.Right))\n\n\tmodel = model.WithRows(rows)\n\n\tconst expectedTable = `┏━━━━━━┳━━━━━━┳━━━━━━┓\n┃  1   ┃     2┃  3   ┃\n┣━━━━━━╋━━━━━━╋━━━━━━┫\n┃   1,1┃   2,1┃   3,1┃\n┃ 1,2  ┃   2,2┃ 3,2  ┃\n┃1,3   ┃2,3   ┃3,3   ┃\n┃ 1,4  ┃   2,4┃ 3,4  ┃\n┃1,5   ┃2,5   ┃3,5   ┃\n┗━━━━━━┻━━━━━━┻━━━━━━┛`\n\n\trendered := model.View()\n\n\tassert.Equal(t, expectedTable, rendered)\n}\n\nfunc TestRowStyleFuncAppliesHighlighted(t *testing.T) {\n\tstyleFunc := func(input RowStyleFuncInput) lipgloss.Style {\n\t\tif input.IsHighlighted {\n\t\t\treturn lipgloss.NewStyle().Align(lipgloss.Center)\n\t\t}\n\n\t\tif input.Index%2 == 0 {\n\t\t\treturn lipgloss.NewStyle().Align(lipgloss.Right)\n\t\t}\n\n\t\treturn lipgloss.NewStyle().Align(lipgloss.Left)\n\t}\n\n\tmodel := New([]Column{\n\t\tNewColumn(\"1\", \"1\", 6),\n\t\tNewColumn(\"2\", \"2\", 6),\n\t\tNewColumn(\"3\", \"3\", 6),\n\t}).\n\t\tWithRowStyleFunc(styleFunc).\n\t\tFocused(true)\n\n\trows := []Row{}\n\n\tfor rowIndex := 1; rowIndex <= 5; rowIndex++ {\n\t\trowData := RowData{}\n\n\t\tfor columnIndex := 1; columnIndex <= 3; columnIndex++ {\n\t\t\tid := fmt.Sprintf(\"%d\", columnIndex)\n\n\t\t\trowData[id] = fmt.Sprintf(\"%d,%d\", columnIndex, rowIndex)\n\t\t}\n\n\t\trows = append(rows, NewRow(rowData))\n\t}\n\n\tmodel = model.WithRows(rows).\n\t\tWithHighlightedRow(2)\n\n\tconst expectedTable = `┏━━━━━━┳━━━━━━┳━━━━━━┓\n┃     1┃     2┃     3┃\n┣━━━━━━╋━━━━━━╋━━━━━━┫\n┃   1,1┃   2,1┃   3,1┃\n┃1,2   ┃2,2   ┃3,2   ┃\n┃ 1,3  ┃ 2,3  ┃ 3,3  ┃\n┃1,4   ┃2,4   ┃3,4   ┃\n┃   1,5┃   2,5┃   3,5┃\n┗━━━━━━┻━━━━━━┻━━━━━━┛`\n\n\trendered := model.View()\n\n\tassert.Equal(t, expectedTable, rendered)\n}\n\n// This is a long test due to typing and multiple big table strings, that's okay\n//\n//nolint:funlen\nfunc Test3x3WithFilterFooter(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"1\", \"1\", 4).WithFiltered(true),\n\t\tNewColumn(\"2\", \"2\", 4),\n\t\tNewColumn(\"3\", \"3\", 4),\n\t})\n\n\trows := []Row{}\n\n\tfor rowIndex := 1; rowIndex <= 3; rowIndex++ {\n\t\trowData := RowData{}\n\n\t\tfor columnIndex := 1; columnIndex <= 3; columnIndex++ {\n\t\t\tid := fmt.Sprintf(\"%d\", columnIndex)\n\n\t\t\trowData[id] = fmt.Sprintf(\"%d,%d\", columnIndex, rowIndex)\n\t\t}\n\n\t\trows = append(rows, NewRow(rowData))\n\t}\n\n\tmodel = model.WithRows(rows).Filtered(true).Focused(true)\n\n\tconst expectedTable = `┏━━━━┳━━━━┳━━━━┓\n┃   1┃   2┃   3┃\n┣━━━━╋━━━━╋━━━━┫\n┃ 1,1┃ 2,1┃ 3,1┃\n┃ 1,2┃ 2,2┃ 3,2┃\n┃ 1,3┃ 2,3┃ 3,3┃\n┣━━━━┻━━━━┻━━━━┫\n┃              ┃\n┗━━━━━━━━━━━━━━┛`\n\n\tassert.Equal(t, expectedTable, model.View())\n\n\thitKey := func(key rune) {\n\t\tmodel, _ = model.Update(\n\t\t\ttea.KeyMsg{\n\t\t\t\tType:  tea.KeyRunes,\n\t\t\t\tRunes: []rune{key},\n\t\t\t})\n\t}\n\n\thitKey('/')\n\thitKey('3')\n\n\t// The byte code near the bottom is a block cursor from the text box\n\tconst expectedFilteredTypingTable = `┏━━━━┳━━━━┳━━━━┓\n┃   1┃   2┃   3┃\n┣━━━━╋━━━━╋━━━━┫\n┃ 1,3┃ 2,3┃ 3,3┃\n┣━━━━┻━━━━┻━━━━┫\n┃           /3` + \"\\x1b[7m \\x1b[0m\" + `┃\n┗━━━━━━━━━━━━━━┛`\n\n\tassert.Equal(t, expectedFilteredTypingTable, model.View())\n\n\tconst expectedFilteredDoneTable = `┏━━━━┳━━━━┳━━━━┓\n┃   1┃   2┃   3┃\n┣━━━━╋━━━━╋━━━━┫\n┃ 1,3┃ 2,3┃ 3,3┃\n┣━━━━┻━━━━┻━━━━┫\n┃            /3┃\n┗━━━━━━━━━━━━━━┛`\n\n\tmodel, _ = model.Update(tea.KeyMsg{Type: tea.KeyEnter})\n\n\tassert.Equal(t, expectedFilteredDoneTable, model.View())\n}\n\nfunc TestSingleCellFlexView(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewFlexColumn(\"id\", \"ID\", 1),\n\t}).WithTargetWidth(6)\n\n\tconst expectedTable = `┏━━━━┓\n┃  ID┃\n┗━━━━┛`\n\n\trendered := model.View()\n\n\tassert.Equal(t, expectedTable, rendered)\n}\n\nfunc TestSimpleFlex3x3(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewFlexColumn(\"1\", \"1\", 1),\n\t\tNewFlexColumn(\"2\", \"2\", 1),\n\t\tNewFlexColumn(\"3\", \"3\", 2),\n\t}).WithTargetWidth(20)\n\n\trows := []Row{}\n\n\tfor rowIndex := 1; rowIndex <= 3; rowIndex++ {\n\t\trowData := RowData{}\n\n\t\tfor columnIndex := 1; columnIndex <= 3; columnIndex++ {\n\t\t\tid := fmt.Sprintf(\"%d\", columnIndex)\n\n\t\t\trowData[id] = fmt.Sprintf(\"%d,%d\", columnIndex, rowIndex)\n\t\t}\n\n\t\trows = append(rows, NewRow(rowData))\n\t}\n\n\tmodel = model.WithRows(rows)\n\n\tconst expectedTable = `┏━━━━┳━━━━┳━━━━━━━━┓\n┃   1┃   2┃       3┃\n┣━━━━╋━━━━╋━━━━━━━━┫\n┃ 1,1┃ 2,1┃     3,1┃\n┃ 1,2┃ 2,2┃     3,2┃\n┃ 1,3┃ 2,3┃     3,3┃\n┗━━━━┻━━━━┻━━━━━━━━┛`\n\n\trendered := model.View()\n\n\tassert.Equal(t, expectedTable, rendered)\n}\n\nfunc TestSimpleFlex3x3AtAllTargetWidths(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"1\", \"1\", 4),\n\t\tNewFlexColumn(\"2\", \"2\", 1),\n\t\tNewFlexColumn(\"3\", \"3\", 2),\n\t}).WithTargetWidth(20)\n\n\trows := []Row{}\n\n\tfor rowIndex := 1; rowIndex <= 3; rowIndex++ {\n\t\trowData := RowData{}\n\n\t\tfor columnIndex := 1; columnIndex <= 3; columnIndex++ {\n\t\t\tid := fmt.Sprintf(\"%d\", columnIndex)\n\n\t\t\trowData[id] = fmt.Sprintf(\"%d,%d\", columnIndex, rowIndex)\n\t\t}\n\n\t\trows = append(rows, NewRow(rowData))\n\t}\n\n\tmodel = model.WithRows(rows)\n\n\tfor targetWidth := 15; targetWidth < 100; targetWidth++ {\n\t\tmodel = model.WithTargetWidth(targetWidth)\n\n\t\trendered := model.View()\n\n\t\tfirstLine := strings.Split(rendered, \"\\n\")[0]\n\n\t\tassert.Equal(t, targetWidth, model.totalWidth)\n\t\tassert.Equal(t, targetWidth, runewidth.StringWidth(firstLine))\n\n\t\tif t.Failed() {\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc TestViewResizesWhenColumnsChange(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"id\", \"ID\", 4),\n\t}).WithRows([]Row{\n\t\tNewRow(RowData{\"id\": \"1\", \"score\": 3}),\n\t\tNewRow(RowData{\"id\": \"2\", \"score\": 4}),\n\t})\n\n\tconst expectedTableOriginal = `┏━━━━┓\n┃  ID┃\n┣━━━━┫\n┃   1┃\n┃   2┃\n┗━━━━┛`\n\n\t// Lowercased, resized, and new column added\n\tconst expectedTableUpdated = `┏━━━━━┳━━━━━━┓\n┃   id┃ Score┃\n┣━━━━━╋━━━━━━┫\n┃    1┃     3┃\n┃    2┃     4┃\n┗━━━━━┻━━━━━━┛`\n\n\trendered := model.View()\n\n\tassert.Equal(t, expectedTableOriginal, rendered)\n\n\tmodel = model.WithColumns([]Column{\n\t\tNewColumn(\"id\", \"id\", 5),\n\t\tNewColumn(\"score\", \"Score\", 6),\n\t})\n\n\trendered = model.View()\n\n\tassert.Equal(t, expectedTableUpdated, rendered)\n}\n\nfunc TestMaxWidthHidesOverflow(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"1\", \"1\", 4),\n\t\tNewColumn(\"2\", \"2\", 4),\n\t\tNewColumn(\"3\", \"3\", 4),\n\t\tNewColumn(\"4\", \"4\", 4),\n\t}).\n\t\tWithRows([]Row{\n\t\t\tNewRow(RowData{\n\t\t\t\t\"1\": \"x1\",\n\t\t\t\t\"2\": \"x2\",\n\t\t\t\t\"3\": \"x3\",\n\t\t\t\t\"4\": \"x4\",\n\t\t\t}),\n\t\t}).\n\t\tWithStaticFooter(\"Footer\").\n\t\t// This includes borders, so should cut off early\n\t\tWithMaxTotalWidth(19)\n\n\tconst expectedTable = `┏━━━━┳━━━━┳━━━━┳━━┓\n┃   1┃   2┃   3┃ >┃\n┣━━━━╋━━━━╋━━━━╋━━┫\n┃  x1┃  x2┃  x3┃ >┃\n┣━━━━┻━━━━┻━━━━┻━━┫\n┃           Footer┃\n┗━━━━━━━━━━━━━━━━━┛`\n\n\trendered := model.View()\n\n\tassert.Equal(t, expectedTable, rendered)\n}\n\nfunc TestMaxWidthHasNoEffectForExactFit(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"1\", \"1\", 4),\n\t\tNewColumn(\"2\", \"2\", 4),\n\t\tNewColumn(\"3\", \"3\", 4),\n\t\tNewColumn(\"4\", \"4\", 4),\n\t}).\n\t\tWithRows([]Row{\n\t\t\tNewRow(RowData{\n\t\t\t\t\"1\": \"x1\",\n\t\t\t\t\"2\": \"x2\",\n\t\t\t\t\"3\": \"x3\",\n\t\t\t\t\"4\": \"x4\",\n\t\t\t}),\n\t\t})\n\n\tconst expectedTable = `┏━━━━┳━━━━┳━━━━┳━━━━┓\n┃   1┃   2┃   3┃   4┃\n┣━━━━╋━━━━╋━━━━╋━━━━┫\n┃  x1┃  x2┃  x3┃  x4┃\n┗━━━━┻━━━━┻━━━━┻━━━━┛`\n\n\tconst expectedTableFooter = `┏━━━━┳━━━━┳━━━━┳━━━━┓\n┃   1┃   2┃   3┃   4┃\n┣━━━━╋━━━━╋━━━━╋━━━━┫\n┃  x1┃  x2┃  x3┃  x4┃\n┣━━━━┻━━━━┻━━━━┻━━━━┫\n┃             Footer┃\n┗━━━━━━━━━━━━━━━━━━━┛`\n\n\tmodel = model.WithMaxTotalWidth(lipgloss.Width(expectedTable))\n\trendered := model.View()\n\tassert.Equal(t, expectedTable, rendered)\n\n\tmodel = model.WithStaticFooter(\"Footer\")\n\trendered = model.View()\n\tassert.Equal(t, expectedTableFooter, rendered)\n}\n\nfunc TestMaxWidthHasNoEffectForSmaller(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"1\", \"1\", 4),\n\t\tNewColumn(\"2\", \"2\", 4),\n\t\tNewColumn(\"3\", \"3\", 4),\n\t\tNewColumn(\"4\", \"4\", 4),\n\t}).\n\t\tWithRows([]Row{\n\t\t\tNewRow(RowData{\n\t\t\t\t\"1\": \"x1\",\n\t\t\t\t\"2\": \"x2\",\n\t\t\t\t\"3\": \"x3\",\n\t\t\t\t\"4\": \"x4\",\n\t\t\t}),\n\t\t})\n\n\tconst expectedTable = `┏━━━━┳━━━━┳━━━━┳━━━━┓\n┃   1┃   2┃   3┃   4┃\n┣━━━━╋━━━━╋━━━━╋━━━━┫\n┃  x1┃  x2┃  x3┃  x4┃\n┗━━━━┻━━━━┻━━━━┻━━━━┛`\n\n\tconst expectedTableFooter = `┏━━━━┳━━━━┳━━━━┳━━━━┓\n┃   1┃   2┃   3┃   4┃\n┣━━━━╋━━━━╋━━━━╋━━━━┫\n┃  x1┃  x2┃  x3┃  x4┃\n┣━━━━┻━━━━┻━━━━┻━━━━┫\n┃             Footer┃\n┗━━━━━━━━━━━━━━━━━━━┛`\n\n\tmodel = model.WithMaxTotalWidth(lipgloss.Width(expectedTable) + 5)\n\trendered := model.View()\n\tassert.Equal(t, expectedTable, rendered)\n\n\tmodel = model.WithStaticFooter(\"Footer\")\n\trendered = model.View()\n\tassert.Equal(t, expectedTableFooter, rendered)\n}\n\nfunc TestMaxWidthHidesOverflowWithSingleCharExtra(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"1\", \"1\", 4),\n\t\tNewColumn(\"2\", \"2\", 4),\n\t\tNewColumn(\"3\", \"3\", 4),\n\t\tNewColumn(\"4\", \"4\", 4),\n\t}).\n\t\tWithRows([]Row{\n\t\t\tNewRow(RowData{\n\t\t\t\t\"1\": \"x1\",\n\t\t\t\t\"2\": \"x2\",\n\t\t\t\t\"3\": \"x3\",\n\t\t\t\t\"4\": \"x4\",\n\t\t\t}),\n\t\t}).\n\t\tWithStaticFooter(\"Footer\").\n\t\t// Juuuust barely overflowing...\n\t\tWithMaxTotalWidth(17)\n\n\tconst expectedTable = `┏━━━━┳━━━━┳━━━━━┓\n┃   1┃   2┃    >┃\n┣━━━━╋━━━━╋━━━━━┫\n┃  x1┃  x2┃    >┃\n┣━━━━┻━━━━┻━━━━━┫\n┃         Footer┃\n┗━━━━━━━━━━━━━━━┛`\n\n\trendered := model.View()\n\n\tassert.Equal(t, expectedTable, rendered)\n}\n\nfunc TestMaxWidthHidesOverflowWithTwoCharExtra(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"1\", \"1\", 4),\n\t\tNewColumn(\"2\", \"2\", 4),\n\t\tNewColumn(\"3\", \"3\", 4),\n\t\tNewColumn(\"4\", \"4\", 4),\n\t}).\n\t\tWithRows([]Row{\n\t\t\tNewRow(RowData{\n\t\t\t\t\"1\": \"x1\",\n\t\t\t\t\"2\": \"x2\",\n\t\t\t\t\"3\": \"x3\",\n\t\t\t\t\"4\": \"x4\",\n\t\t\t}),\n\t\t}).\n\t\t// Just enough to squeeze in a '>' column\n\t\tWithMaxTotalWidth(18)\n\n\tconst expectedTable = `┏━━━━┳━━━━┳━━━━┳━┓\n┃   1┃   2┃   3┃>┃\n┣━━━━╋━━━━╋━━━━╋━┫\n┃  x1┃  x2┃  x3┃>┃\n┗━━━━┻━━━━┻━━━━┻━┛`\n\n\tconst expectedTableFooter = `┏━━━━┳━━━━┳━━━━┳━┓\n┃   1┃   2┃   3┃>┃\n┣━━━━╋━━━━╋━━━━╋━┫\n┃  x1┃  x2┃  x3┃>┃\n┣━━━━┻━━━━┻━━━━┻━┫\n┃          Footer┃\n┗━━━━━━━━━━━━━━━━┛`\n\n\trendered := model.View()\n\tassert.Equal(t, expectedTable, rendered)\n\n\tmodel = model.WithStaticFooter(\"Footer\")\n\trendered = model.View()\n\tassert.Equal(t, expectedTableFooter, rendered)\n}\n\nfunc TestScrolledTableSizesFooterCorrectly(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"1\", \"1\", 4),\n\t\tNewColumn(\"2\", \"2\", 4),\n\t\tNewColumn(\"3\", \"3\", 4),\n\t\tNewColumn(\"4\", \"4\", 4),\n\t}).\n\t\tWithRows([]Row{\n\t\t\tNewRow(RowData{\n\t\t\t\t\"1\": \"x1\",\n\t\t\t\t\"2\": \"x2\",\n\t\t\t\t\"3\": \"x3\",\n\t\t\t\t\"4\": \"x4\",\n\t\t\t}),\n\t\t}).\n\t\tWithMaxTotalWidth(19).\n\t\tWithStaticFooter(\"Footer\").\n\t\tScrollRight()\n\n\tconst expectedTable = `┏━┳━━━━┳━━━━┳━━━━┓\n┃<┃   2┃   3┃   4┃\n┣━╋━━━━╋━━━━╋━━━━┫\n┃<┃  x2┃  x3┃  x4┃\n┣━┻━━━━┻━━━━┻━━━━┫\n┃          Footer┃\n┗━━━━━━━━━━━━━━━━┛`\n\n\trendered := model.View()\n\tassert.Equal(t, expectedTable, rendered)\n}\n\nfunc TestHorizontalScrollCaretIsRightAligned(t *testing.T) {\n\tleftAlign := lipgloss.NewStyle().Align(lipgloss.Left)\n\tmodel := New([]Column{\n\t\tNewColumn(\"1\", \"1\", 4),\n\t\tNewColumn(\"2\", \"2\", 4),\n\t\tNewColumn(\"3\", \"3\", 4),\n\t\tNewColumn(\"4\", \"4\", 4),\n\t}).\n\t\tWithRows([]Row{\n\t\t\tNewRow(RowData{\n\t\t\t\t\"1\": \"x1\",\n\t\t\t\t\"2\": \"x2\",\n\t\t\t\t\"3\": \"x3\",\n\t\t\t\t\"4\": \"x4\",\n\t\t\t}).WithStyle(leftAlign),\n\t\t}).\n\t\tHeaderStyle(leftAlign).\n\t\tWithStaticFooter(\"Footer\").\n\t\tWithMaxTotalWidth(17)\n\n\tconst expectedTable = `┏━━━━┳━━━━┳━━━━━┓\n┃1   ┃2   ┃    >┃\n┣━━━━╋━━━━╋━━━━━┫\n┃x1  ┃x2  ┃    >┃\n┣━━━━┻━━━━┻━━━━━┫\n┃         Footer┃\n┗━━━━━━━━━━━━━━━┛`\n\n\trendered := model.View()\n\n\tassert.Equal(t, expectedTable, rendered)\n}\n\nfunc Test3x3WithRoundedBorder(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"1\", \"1\", 4),\n\t\tNewColumn(\"2\", \"2\", 4),\n\t\tNewColumn(\"3\", \"3\", 4),\n\t}).BorderRounded()\n\n\trows := []Row{}\n\n\tfor rowIndex := 1; rowIndex <= 3; rowIndex++ {\n\t\trowData := RowData{}\n\n\t\tfor columnIndex := 1; columnIndex <= 3; columnIndex++ {\n\t\t\tid := fmt.Sprintf(\"%d\", columnIndex)\n\n\t\t\trowData[id] = fmt.Sprintf(\"%d,%d\", columnIndex, rowIndex)\n\t\t}\n\n\t\trows = append(rows, NewRow(rowData))\n\t}\n\n\tmodel = model.WithRows(rows).WithStaticFooter(\"Footer\")\n\n\tconst expectedTable = `╭────┬────┬────╮\n│   1│   2│   3│\n├────┼────┼────┤\n│ 1,1│ 2,1│ 3,1│\n│ 1,2│ 2,2│ 3,2│\n│ 1,3│ 2,3│ 3,3│\n├────┴────┴────┤\n│        Footer│\n╰──────────────╯`\n\n\trendered := model.View()\n\n\tassert.Equal(t, expectedTable, rendered)\n}\n\nfunc TestSingleColumnViewSortedAndFormatted(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"name\", \"Name\", 5),\n\t\tNewColumn(\"val\", \"Value\", 7).\n\t\t\tWithFormatString(\"~%.2f\"),\n\t}).WithRows([]Row{\n\t\tNewRow(RowData{\"name\": \"π\", \"val\": 3.14}),\n\t\tNewRow(RowData{\"name\": \"Φ\", \"val\": 1.618}),\n\t}).SortByAsc(\"val\")\n\n\tconst expectedTable = `┏━━━━━┳━━━━━━━┓\n┃ Name┃  Value┃\n┣━━━━━╋━━━━━━━┫\n┃    Φ┃  ~1.62┃\n┃    π┃  ~3.14┃\n┗━━━━━┻━━━━━━━┛`\n\n\trendered := model.View()\n\n\tassert.Equal(t, expectedTable, rendered)\n}\n\nfunc TestMinimumHeightSingleCellView(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"id\", \"ID\", 4),\n\t}).WithMinimumHeight(5)\n\n\tconst expectedTable = `┏━━━━┓\n┃  ID┃\n┣━━━━┫\n┃    ┃\n┗━━━━┛`\n\n\trendered := model.View()\n\n\tassert.Equal(t, expectedTable, rendered)\n}\n\nfunc TestMinimumHeightSingleColumnView(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"id\", \"ID\", 4),\n\t}).WithRows([]Row{\n\t\tNewRow(RowData{\"id\": \"1\"}),\n\t\tNewRow(RowData{\"id\": \"2\"}),\n\t}).WithMinimumHeight(8)\n\n\tconst expectedTable = `┏━━━━┓\n┃  ID┃\n┣━━━━┫\n┃   1┃\n┃   2┃\n┃    ┃\n┃    ┃\n┗━━━━┛`\n\n\trendered := model.View()\n\n\tassert.Equal(t, expectedTable, rendered)\n}\n\nfunc TestMinimumHeightHeaderNoData(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"1\", \"1\", 4),\n\t\tNewColumn(\"2\", \"2\", 4),\n\t\tNewColumn(\"3\", \"3\", 4),\n\t}).WithMinimumHeight(5)\n\n\tconst expectedTable = `┏━━━━┳━━━━┳━━━━┓\n┃   1┃   2┃   3┃\n┣━━━━╋━━━━╋━━━━┫\n┃    ┃    ┃    ┃\n┗━━━━┻━━━━┻━━━━┛`\n\n\trendered := model.View()\n\n\tassert.Equal(t, expectedTable, rendered)\n}\n\nfunc TestMinimumHeightSingleRowWithHiddenHeader(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"1\", \"1\", 4),\n\t\tNewColumn(\"2\", \"2\", 4),\n\t\tNewColumn(\"3\", \"3\", 4),\n\t}).\n\t\tWithHeaderVisibility(false).\n\t\tWithRows([]Row{\n\t\t\tNewRow(RowData{\"1\": \"a\", \"2\": \"b\", \"3\": \"c\"}),\n\t\t}).\n\t\tWithMinimumHeight(4)\n\n\tconst expectedTable = `┏━━━━┳━━━━┳━━━━┓\n┃   a┃   b┃   c┃\n┃    ┃    ┃    ┃\n┗━━━━┻━━━━┻━━━━┛`\n\n\trendered := model.View()\n\n\tassert.Equal(t, expectedTable, rendered)\n}\n\nfunc TestMinimumHeightNoRowsAndHiddenHeader(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"1\", \"1\", 4),\n\t\tNewColumn(\"2\", \"2\", 4),\n\t\tNewColumn(\"3\", \"3\", 4),\n\t}).WithHeaderVisibility(false).WithMinimumHeight(3)\n\n\tconst expectedTable = `┏━━━━┳━━━━┳━━━━┓\n┃    ┃    ┃    ┃\n┗━━━━┻━━━━┻━━━━┛`\n\n\trendered := model.View()\n\n\tassert.Equal(t, expectedTable, rendered)\n}\n\nfunc TestMinimumHeightSingleColumnNoDataWithFooter(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"id\", \"ID\", 4),\n\t}).WithStaticFooter(\"Foot\").WithMinimumHeight(7)\n\n\tconst expectedTable = `┏━━━━┓\n┃  ID┃\n┣━━━━┫\n┃    ┃\n┣━━━━┫\n┃Foot┃\n┗━━━━┛`\n\trendered := model.View()\n\n\tassert.Equal(t, expectedTable, rendered)\n}\n\nfunc TestMinimumHeightSingleColumnWithFooterAndHiddenHeader(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"id\", \"ID\", 4),\n\t}).\n\t\tWithStaticFooter(\"Foot\").\n\t\tWithHeaderVisibility(false).\n\t\tWithMinimumHeight(6)\n\n\tconst expectedTable = `┏━━━━┓\n┃    ┃\n┃    ┃\n┣━━━━┫\n┃Foot┃\n┗━━━━┛`\n\trendered := model.View()\n\n\tassert.Equal(t, expectedTable, rendered)\n}\n\nfunc TestMinimumHeightSingleRowWithFooter(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"1\", \"1\", 4),\n\t\tNewColumn(\"2\", \"2\", 4),\n\t\tNewColumn(\"3\", \"3\", 4),\n\t}).WithStaticFooter(\"Footer\").WithMinimumHeight(7)\n\n\tconst expectedTable = `┏━━━━┳━━━━┳━━━━┓\n┃   1┃   2┃   3┃\n┣━━━━╋━━━━╋━━━━┫\n┃    ┃    ┃    ┃\n┣━━━━┻━━━━┻━━━━┫\n┃        Footer┃\n┗━━━━━━━━━━━━━━┛`\n\n\trendered := model.View()\n\n\tassert.Equal(t, expectedTable, rendered)\n}\n\nfunc TestMinimumHeightSingleColumnWithFooter(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"id\", \"ID\", 4),\n\t}).WithRows([]Row{\n\t\tNewRow(RowData{\"id\": \"1\"}),\n\t\tNewRow(RowData{\"id\": \"2\"}),\n\t}).WithStaticFooter(\"Foot\").WithMinimumHeight(9)\n\n\tconst expectedTable = `┏━━━━┓\n┃  ID┃\n┣━━━━┫\n┃   1┃\n┃   2┃\n┃    ┃\n┣━━━━┫\n┃Foot┃\n┗━━━━┛`\n\n\trendered := model.View()\n\n\tassert.Equal(t, expectedTable, rendered)\n}\n\nfunc TestMinimumHeightExtraRow(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"id\", \"ID\", 4),\n\t}).WithStaticFooter(\"Foot\").WithMinimumHeight(6)\n\n\tconst expectedTable = `┏━━━━┓\n┃  ID┃\n┣━━━━┫\n┃    ┃\n┣━━━━┫\n┃Foot┃\n┗━━━━┛`\n\n\trendered := model.View()\n\n\tassert.Equal(t, expectedTable, rendered)\n}\n\nfunc TestMinimumHeightSmallerThanTable(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"id\", \"ID\", 4),\n\t}).WithRows([]Row{\n\t\tNewRow(RowData{\"id\": \"1\"}),\n\t\tNewRow(RowData{\"id\": \"2\"}),\n\t}).WithStaticFooter(\"Foot\").WithMinimumHeight(7)\n\n\tconst expectedTable = `┏━━━━┓\n┃  ID┃\n┣━━━━┫\n┃   1┃\n┃   2┃\n┣━━━━┫\n┃Foot┃\n┗━━━━┛`\n\n\trendered := model.View()\n\n\tassert.Equal(t, expectedTable, rendered)\n}\n\nfunc TestMultilineEnabled(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"name\", \"Name\", 4),\n\t}).\n\t\tWithRows([]Row{\n\t\t\tNewRow(RowData{\"name\": \"AAAAAAAAAAAAAAAAAA\"}),\n\t\t\tNewRow(RowData{\"name\": \"BBB\"}),\n\t\t}).\n\t\tWithMultiline(true)\n\n\tassert.True(t, model.multiline)\n\n\tconst expectedTable = `┏━━━━┓\n┃Name┃\n┣━━━━┫\n┃AAAA┃\n┃AAAA┃\n┃AAAA┃\n┃AAAA┃\n┃AA  ┃\n┃BBB ┃\n┗━━━━┛`\n\n\trendered := model.View()\n\tassert.Equal(t, expectedTable, rendered)\n}\n\nfunc TestMultilineDisabledByDefault(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"name\", \"Name\", 4),\n\t}).\n\t\tWithRows([]Row{\n\t\t\tNewRow(RowData{\"name\": \"AAAAAAAAAAAAAAAAAA\"}),\n\t\t\tNewRow(RowData{\"name\": \"BBB\"}),\n\t\t})\n\t\t// WithMultiline(false)\n\n\tassert.False(t, model.multiline)\n\n\tconst expectedTable = `┏━━━━┓\n┃Name┃\n┣━━━━┫\n┃AAA…┃\n┃ BBB┃\n┗━━━━┛`\n\n\trendered := model.View()\n\tassert.Equal(t, expectedTable, rendered)\n}\n\nfunc TestMultilineDisabledExplicite(t *testing.T) {\n\tmodel := New([]Column{\n\t\tNewColumn(\"name\", \"Name\", 4),\n\t}).\n\t\tWithRows([]Row{\n\t\t\tNewRow(RowData{\"name\": \"AAAAAAAAAAAAAAAAAA\"}),\n\t\t\tNewRow(RowData{\"name\": \"BBB\"}),\n\t\t}).\n\t\tWithMultiline(false)\n\n\tassert.False(t, model.multiline)\n\n\tconst expectedTable = `┏━━━━┓\n┃Name┃\n┣━━━━┫\n┃AAA…┃\n┃ BBB┃\n┗━━━━┛`\n\n\trendered := model.View()\n\tassert.Equal(t, expectedTable, rendered)\n}\n"
  }
]