Full Code of charmbracelet/bubbles for AI

main d2b804ead271 cached
98 files
390.4 KB
115.7k tokens
683 symbols
1 requests
Download .txt
Showing preview only (418K chars total). Download the full file or copy to clipboard to get everything.
Repository: charmbracelet/bubbles
Branch: main
Commit: d2b804ead271
Files: 98
Total size: 390.4 KB

Directory structure:
gitextract_bq3bte_v/

├── .github/
│   ├── CODEOWNERS
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   ├── config.yml
│   │   └── feature_request.md
│   ├── dependabot.yml
│   └── workflows/
│       ├── build.yml
│       ├── coverage.yml
│       ├── dependabot-sync.yml
│       ├── lint-sync.yml
│       ├── lint.yml
│       └── release.yml
├── .gitignore
├── .golangci.yml
├── .goreleaser.yml
├── LICENSE
├── README.md
├── Taskfile.yaml
├── UPGRADE_GUIDE_V2.md
├── bubbles.go
├── cursor/
│   ├── cursor.go
│   └── cursor_test.go
├── filepicker/
│   ├── filepicker.go
│   ├── hidden_unix.go
│   └── hidden_windows.go
├── go.mod
├── go.sum
├── help/
│   ├── help.go
│   ├── help_test.go
│   └── testdata/
│       └── TestFullHelp/
│           ├── full_help_20_width.golden
│           ├── full_help_30_width.golden
│           └── full_help_40_width.golden
├── internal/
│   ├── memoization/
│   │   ├── memoization.go
│   │   └── memoization_test.go
│   └── runeutil/
│       ├── runeutil.go
│       └── runeutil_test.go
├── key/
│   ├── key.go
│   └── key_test.go
├── list/
│   ├── README.md
│   ├── defaultitem.go
│   ├── keys.go
│   ├── list.go
│   ├── list_test.go
│   └── style.go
├── paginator/
│   ├── paginator.go
│   └── paginator_test.go
├── progress/
│   ├── progress.go
│   ├── progress_test.go
│   └── testdata/
│       └── TestBlend/
│           ├── 10w-red-to-green-50perc-full-block.golden
│           ├── 10w-red-to-green-50perc.golden
│           ├── 10w-red-to-green-scaled-50perc.golden
│           ├── 30w-colorfunc-rgb-100perc.golden
│           ├── 30w-red-to-green-100perc.golden
│           └── 30w-red-to-green-scaled-100perc.golden
├── spinner/
│   ├── spinner.go
│   └── spinner_test.go
├── stopwatch/
│   └── stopwatch.go
├── table/
│   ├── table.go
│   ├── table_test.go
│   └── testdata/
│       ├── TestModel_View/
│       │   ├── Bordered_cells.golden
│       │   ├── Bordered_headers.golden
│       │   ├── Empty.golden
│       │   ├── Extra_padding.golden
│       │   ├── Height_greater_than_rows.golden
│       │   ├── Height_less_than_rows.golden
│       │   ├── Modified_viewport_height.golden
│       │   ├── Multiple_rows_and_columns.golden
│       │   ├── No_padding.golden
│       │   ├── Single_row_and_column.golden
│       │   ├── Width_greater_than_columns.golden
│       │   └── Width_less_than_columns.golden
│       ├── TestModel_View_CenteredInABox.golden
│       └── TestTableAlignment/
│           ├── No_border.golden
│           └── With_border.golden
├── textarea/
│   ├── textarea.go
│   └── textarea_test.go
├── textinput/
│   ├── styles.go
│   ├── textinput.go
│   └── textinput_test.go
├── timer/
│   └── timer.go
└── viewport/
    ├── highlight.go
    ├── keymap.go
    ├── testdata/
    │   └── TestSizing/
    │       ├── view-40x1-softwrap-at-bottom.golden
    │       ├── view-40x1-softwrap-scrolled-plus-1.golden
    │       ├── view-40x1-softwrap-scrolled-plus-2.golden
    │       ├── view-40x1-softwrap.golden
    │       ├── view-40x1.golden
    │       ├── view-40x100percent.golden
    │       ├── view-50x15-content-lines.golden
    │       ├── view-50x15-softwrap-at-bottom.golden
    │       ├── view-50x15-softwrap-at-top.golden
    │       ├── view-50x15-softwrap-gutter-at-bottom.golden
    │       ├── view-50x15-softwrap-gutter-at-top.golden
    │       ├── view-50x15-softwrap-gutter-scrolled-plus-1.golden
    │       ├── view-50x15-softwrap-gutter-scrolled-plus-2.golden
    │       ├── view-50x15-softwrap-scrolled-plus-1.golden
    │       └── view-50x15-softwrap-scrolled-plus-2.golden
    ├── viewport.go
    └── viewport_test.go

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/CODEOWNERS
================================================
*  @meowgorithm @bashbunni
cursor/  @aymanbagabas
filepicker/  @bashbunni
help/  @meowgorithm
key/  @meowgorithm
list/  @meowgorithm
paginator/  @meowgorithm
progress/  @meowgorithm
spinner/  @meowgorithm
stopwatch/  @caarlos0
table/  @aymanbagabas
textarea/  @aymanbagabas
textinput/  @meowgorithm
viewport/  @meowgorithm


================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''

---

**Describe the bug**
A clear and concise description of what the bug is.

**Setup**
Please complete the following information along with version numbers, if applicable.
 - OS [e.g. Ubuntu, macOS]
 - Shell [e.g. zsh, fish]
 - Terminal Emulator [e.g. kitty, iterm]
 - Terminal Multiplexer [e.g. tmux]
 - Locale [e.g. en_US.UTF-8, zh_CN.UTF-8, etc.]

**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error

**Source Code**
Please include source code if needed to reproduce the behavior. 

**Expected behavior**
A clear and concise description of what you expected to happen.

**Screenshots**
Add screenshots to help explain your problem.

**Additional context**
Add any other context about the problem here.


================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: true
contact_links:
- name: Discord
  url: https://charm.sh/discord
  about: Chat on our Discord.


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''

---

**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

**Describe the solution you'd like**
A clear and concise description of what you want to happen.

**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

**Additional context**
Add any other context or screenshots about the feature request here.


================================================
FILE: .github/dependabot.yml
================================================
version: 2

updates:
  - package-ecosystem: "gomod"
    directory: "/"
    schedule:
      interval: "weekly"
      day: "monday"
      time: "05:00"
      timezone: "America/New_York"
    labels:
      - "dependencies"
    commit-message:
      prefix: "chore"
      include: "scope"
    groups:
      all:
        patterns:
          - "*"

  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"
      day: "monday"
      time: "05:00"
      timezone: "America/New_York"
    labels:
      - "dependencies"
    commit-message:
      prefix: "chore"
      include: "scope"
    groups:
      all:
        patterns:
          - "*"

  - package-ecosystem: "docker"
    directory: "/"
    schedule:
      interval: "weekly"
      day: "monday"
      time: "05:00"
      timezone: "America/New_York"
    labels:
      - "dependencies"
    commit-message:
      prefix: "chore"
      include: "scope"
    groups:
      all:
        patterns:
          - "*"


================================================
FILE: .github/workflows/build.yml
================================================
name: build

on: [push, pull_request]

jobs:
  test:
    strategy:
      matrix:
        go-version: [stable, oldstable]
        os: [ubuntu-latest, macos-latest, windows-latest]
    runs-on: ${{ matrix.os }}
    steps:
      - name: Install Go
        uses: actions/setup-go@v6
        with:
          go-version: ${{ matrix.go-version }}

      - name: Checkout code
        uses: actions/checkout@v6

      - name: Download Go modules
        run: go mod download

      - name: Build
        run: go build -v ./...

      - name: Test
        run: go test ./...

  dependabot:
    needs: [test]
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write
      contents: write
    if: ${{ github.actor == 'dependabot[bot]' && github.event_name == 'pull_request'}}
    steps:
      - id: metadata
        uses: dependabot/fetch-metadata@v2
        with:
          github-token: "${{ secrets.GITHUB_TOKEN }}"
      - run: |
          gh pr review --approve "$PR_URL"
          gh pr merge --squash --auto "$PR_URL"
        env:
          PR_URL: ${{github.event.pull_request.html_url}}
          GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}


================================================
FILE: .github/workflows/coverage.yml
================================================
name: coverage
on: [push, pull_request]

jobs:
  coverage:
    strategy:
      matrix:
        go-version: [^1.22]
        os: [ubuntu-latest]
    runs-on: ${{ matrix.os }}
    env:
      GO111MODULE: "on"
    steps:
      - name: Install Go
        uses: actions/setup-go@v6
        with:
          go-version: ${{ matrix.go-version }}

      - name: Checkout code
        uses: actions/checkout@v6

      - name: Coverage
        env:
          COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          go test -race -covermode atomic -coverprofile=profile.cov ./...
          go install github.com/mattn/goveralls@latest
          goveralls -coverprofile=profile.cov -service=github


================================================
FILE: .github/workflows/dependabot-sync.yml
================================================
name: dependabot-sync
on:
  schedule:
    - cron: "0 0 * * 0" # every Sunday at midnight
  workflow_dispatch: # allows manual triggering

permissions:
  contents: write
  pull-requests: write

jobs:
  dependabot-sync:
    uses: charmbracelet/meta/.github/workflows/dependabot-sync.yml@main
    with:
      repo_name: ${{ github.event.repository.name }}
    secrets:
      gh_token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}


================================================
FILE: .github/workflows/lint-sync.yml
================================================
name: lint-sync
on:
  schedule:
    # every Sunday at midnight
    - cron: "0 0 * * 0"
  workflow_dispatch: # allows manual triggering

permissions:
  contents: write
  pull-requests: write

jobs:
  lint:
    uses: charmbracelet/meta/.github/workflows/lint-sync.yml@main


================================================
FILE: .github/workflows/lint.yml
================================================
name: lint
on:
  push:
  pull_request:

jobs:
  lint:
    uses: charmbracelet/meta/.github/workflows/lint.yml@main
    with:
      golangci_path: .golangci.yml
      golangci_version: v2.9
      timeout: 10m


================================================
FILE: .github/workflows/release.yml
================================================
name: goreleaser

on:
  push:
    tags:
      - v*.*.*

concurrency:
  group: goreleaser
  cancel-in-progress: true

jobs:
  goreleaser:
    uses: charmbracelet/meta/.github/workflows/goreleaser.yml@main
    secrets:
      docker_username: ${{ secrets.DOCKERHUB_USERNAME }}
      docker_token: ${{ secrets.DOCKERHUB_TOKEN }}
      gh_pat: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
      goreleaser_key: ${{ secrets.GORELEASER_KEY }}
      twitter_consumer_key: ${{ secrets.TWITTER_CONSUMER_KEY }}
      twitter_consumer_secret: ${{ secrets.TWITTER_CONSUMER_SECRET }}
      twitter_access_token: ${{ secrets.TWITTER_ACCESS_TOKEN }}
      twitter_access_token_secret: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }}
      mastodon_client_id: ${{ secrets.MASTODON_CLIENT_ID }}
      mastodon_client_secret: ${{ secrets.MASTODON_CLIENT_SECRET }}
      mastodon_access_token: ${{ secrets.MASTODON_ACCESS_TOKEN }}
      discord_webhook_id: ${{ secrets.DISCORD_WEBHOOK_ID }}
      discord_webhook_token: ${{ secrets.DISCORD_WEBHOOK_TOKEN }}
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json


================================================
FILE: .gitignore
================================================
.DS_Store
dist/


================================================
FILE: .golangci.yml
================================================
version: "2"
run:
  tests: false
linters:
  enable:
    - bodyclose
    - exhaustive
    - goconst
    - godot
    - godox
    - gomoddirectives
    - goprintffuncname
    - gosec
    - misspell
    - nakedret
    - nestif
    - nilerr
    - noctx
    - nolintlint
    - prealloc
    - revive
    - rowserrcheck
    - sqlclosecheck
    - tparallel
    - unconvert
    - unparam
    - whitespace
    - wrapcheck
  exclusions:
    generated: lax
    presets:
      - common-false-positives
    rules:
      - linters:
          - revive
        text: "var-naming"
issues:
  max-issues-per-linter: 0
  max-same-issues: 0
formatters:
  enable:
    - gofumpt
    - goimports
  exclusions:
    generated: lax


================================================
FILE: .goreleaser.yml
================================================
# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json
version: 2
includes:
  - from_url:
      url: charmbracelet/meta/main/goreleaser-lib.yaml


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2020-2026 Charmbracelet, Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# Bubbles

<img src="https://github.com/user-attachments/assets/b89fa46e-d451-4b33-a009-c68d4765520f" width="350" />

[![Latest Release](https://img.shields.io/github/release/charmbracelet/bubbles.svg)](https://github.com/charmbracelet/bubbles/releases)
[![GoDoc](https://godoc.org/github.com/golang/gddo?status.svg)](https://pkg.go.dev/github.com/charmbracelet/bubbles)
[![Build Status](https://github.com/charmbracelet/bubbles/workflows/build/badge.svg)](https://github.com/charmbracelet/bubbles/actions)
[![Go ReportCard](https://goreportcard.com/badge/charmbracelet/bubbles)](https://goreportcard.com/report/charmbracelet/bubbles)

Primatives for [Bubble Tea](https://github.com/charmbracelet/bubbletea)
applications. These components are used in production in [Crush][crush], and [many other applications][otherstuff].

> [!TIP]
>
> Upgrading from v1? Check out the [upgrade guide](./UPGRADE_GUIDE_V2.md), or
> point your LLM at it and let it go to town.

[crush]: https://github.com/charmbracelet/crush
[otherstuff]: https://github.com/charmbracelet/bubbletea/#bubble-tea-in-the-wild

## Spinner

<img src="https://stuff.charm.sh/bubbles-examples/spinner.gif" width="400" alt="Spinner Example">

A spinner, useful for indicating that some kind an operation is happening.
There are a couple default ones, but you can also pass your own ”frames.”

- [Example code, basic spinner](https://github.com/charmbracelet/bubbletea/blob/main/examples/spinner/main.go)
- [Example code, various spinners](https://github.com/charmbracelet/bubbletea/blob/main/examples/spinners/main.go)

## Text Input

<img src="https://stuff.charm.sh/bubbles-examples/textinput.gif" width="400" alt="Text Input Example">

A text input field, akin to an `<input type="text">` in HTML. Supports unicode,
pasting, in-place scrolling when the value exceeds the width of the element and
the common, and many customization options.

- [Example code, one field](https://github.com/charmbracelet/bubbletea/blob/main/examples/textinput/main.go)
- [Example code, many fields](https://github.com/charmbracelet/bubbletea/blob/main/examples/textinputs/main.go)

## Text Area

<img src="https://stuff.charm.sh/bubbles-examples/textarea.gif" width="400" alt="Text Area Example">

A text area field, akin to an `<textarea />` in HTML. Allows for input that
spans multiple lines. Supports unicode, pasting, vertical scrolling when the
value exceeds the width and height of the element, and many customization
options.

- [Example code, chat input](https://github.com/charmbracelet/bubbletea/blob/main/examples/chat/main.go)
- [Example code, story time input](https://github.com/charmbracelet/bubbletea/blob/main/examples/textarea/main.go)

## Table

<img src="https://stuff.charm.sh/bubbles-examples/table.gif" width="400" alt="Table Example">

A component for displaying and navigating tabular data (columns and rows).
Supports vertical scrolling and many customization options.

- [Example code, countries and populations](https://github.com/charmbracelet/bubbletea/blob/main/examples/table/main.go)

## Progress

<img src="https://stuff.charm.sh/bubbles-examples/progress.gif" width="800" alt="Progressbar Example">

A simple, customizable progress meter, with optional animation via
[Harmonica][harmonica]. Supports solid and gradient fills. The empty and filled
runes can be set to whatever you'd like. The percentage readout is customizable
and can also be omitted entirely.

- [Animated example](https://github.com/charmbracelet/bubbletea/blob/main/examples/progress-animated/main.go)
- [Static example](https://github.com/charmbracelet/bubbletea/blob/main/examples/progress-static/main.go)

[harmonica]: https://github.com/charmbracelet/harmonica

## Paginator

<img src="https://stuff.charm.sh/bubbles-examples/pagination.gif" width="200" alt="Paginator Example">

A component for handling pagination logic and optionally drawing pagination UI.
Supports "dot-style" pagination (similar to what you might see on iOS) and
numeric page numbering, but you could also just use this component for the
logic and visualize pagination however you like.

- [Example code](https://github.com/charmbracelet/bubbletea/blob/main/examples/paginator/main.go)

## Viewport

<img src="https://stuff.charm.sh/bubbles-examples/viewport.gif" width="600" alt="Viewport Example">

A viewport for vertically scrolling content. Optionally includes standard
pager keybindings and mouse wheel support. A high performance mode is available
for applications which make use of the alternate screen buffer.

- [Example code](https://github.com/charmbracelet/bubbletea/blob/main/examples/pager/main.go)

This component is well complemented with [Reflow][reflow] for ANSI-aware
indenting and text wrapping.

[reflow]: https://github.com/muesli/reflow

## List

<img src="https://stuff.charm.sh/bubbles-examples/list.gif" width="600" alt="List Example">

A customizable, batteries-included component for browsing a set of items.
Features pagination, fuzzy filtering, auto-generated help, an activity spinner,
and status messages, all of which can be enabled and disabled as needed.
Extrapolated from [Glow][glow].

- [Example code, default list](https://github.com/charmbracelet/bubbletea/blob/main/examples/list-default/main.go)
- [Example code, simple list](https://github.com/charmbracelet/bubbletea/blob/main/examples/list-simple/main.go)
- [Example code, all features](https://github.com/charmbracelet/bubbletea/blob/main/examples/list-fancy/main.go)

## File Picker

<img src="https://vhs.charm.sh/vhs-yET2HNiJNEbyqaVfYuLnY.gif" width="600" alt="File picker example">

A customizable component for picking a file from the file system. Navigate
through directories and select files, optionally limit to certain file
extensions.

- [Example code](https://github.com/charmbracelet/bubbletea/blob/main/examples/file-picker/main.go)

## Timer

A simple, flexible component for counting down. The update frequency and output
can be customized as you like.

<img src="https://stuff.charm.sh/bubbles-examples/timer.gif" width="400" alt="Timer example">

- [Example code](https://github.com/charmbracelet/bubbletea/blob/main/examples/timer/main.go)

## Stopwatch

<img src="https://stuff.charm.sh/bubbles-examples/stopwatch.gif" width="400" alt="Stopwatch example">

A simple, flexible component for counting up. The update frequency and output
can be customized as you see fit.

- [Example code](https://github.com/charmbracelet/bubbletea/blob/main/examples/stopwatch/main.go)

## Help

<img src="https://stuff.charm.sh/bubbles-examples/help.gif" width="500" alt="Help Example">

A customizable horizontal mini help view that automatically generates itself
from your keybindings. It features single and multi-line modes, which the user
can optionally toggle between. It will truncate gracefully if the terminal is
too wide for the content.

- [Example code](https://github.com/charmbracelet/bubbletea/blob/main/examples/help/main.go)

## Key

A non-visual component for managing keybindings. It’s useful for allowing users
to remap keybindings as well as generating help views corresponding to your
keybindings.

```go
type KeyMap struct {
    Up key.Binding
    Down key.Binding
}

var DefaultKeyMap = KeyMap{
    Up: key.NewBinding(
        key.WithKeys("k", "up"),        // actual keybindings
        key.WithHelp("↑/k", "move up"), // corresponding help text
    ),
    Down: key.NewBinding(
        key.WithKeys("j", "down"),
        key.WithHelp("↓/j", "move down"),
    ),
}

func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.KeyPressMsg:
        switch {
        case key.Matches(msg, DefaultKeyMap.Up):
            // The user pressed up
        case key.Matches(msg, DefaultKeyMap.Down):
            // The user pressed down
        }
    }
    return m, nil
}
```

## There’s more where that came from

To check out community-maintained Bubbles see [Charm & Friends][charmandfriends].
Made a cool Bubble that you want to share? [PRs][prs] are welcome!

[charmandfriends]: https://github.com/charm-and-friends/additional-bubbles
[prs]: https://github.com/charm-and-friends/additional-bubbles?tab=readme-ov-file#what-is-a-complete-project

## Contributing

See [contributing][contribute].

[contribute]: https://github.com/charmbracelet/bubbles/contribute

## Feedback

We’d love to hear your thoughts on this project. Feel free to drop us a note!

- [Twitter](https://twitter.com/charmcli)
- [The Fediverse](https://mastodon.social/@charmcli)
- [Discord](https://charm.sh/chat)

## License

[MIT](https://github.com/charmbracelet/bubbletea/raw/main/LICENSE)

---

Part of [Charm](https://charm.land).

<a href="https://charm.sh/"><img alt="The Charm logo" src="https://stuff.charm.sh/charm-banner-next.jpg" width="400"></a>

Charm热爱开源 • Charm loves open source


================================================
FILE: Taskfile.yaml
================================================
# https://taskfile.dev

version: '3'

tasks:
  lint:
    desc: Run lint
    cmds:
      - golangci-lint run

  test:
    desc: Run tests
    cmds:
      - go test ./... {{.CLI_ARGS}}


================================================
FILE: UPGRADE_GUIDE_V2.md
================================================
# Upgrading to Bubbles v2

This guide covers every breaking change when migrating from Bubbles v1 (`github.com/charmbracelet/bubbles`) to Bubbles v2 (`charm.land/bubbles/v2`). It is written for both humans and LLM-assisted migration tools.

> **Companion upgrades required.** Bubbles v2 requires Bubble Tea v2 and Lip Gloss v2. Upgrade all three together:
>
> ```sh
> go get charm.land/bubbletea/v2@latest
> go get charm.land/bubbles/v2@latest
> go get charm.land/lipgloss/v2@latest
> ```

---

## Table of Contents

1. [Import Paths](#1-import-paths)
2. [Global Patterns](#2-global-patterns)
3. [Per-Component Migration](#3-per-component-migration)
   - [Cursor](#cursor)
   - [Filepicker](#filepicker)
   - [Help](#help)
   - [List](#list)
   - [Paginator](#paginator)
   - [Progress](#progress)
   - [Spinner](#spinner)
   - [Stopwatch](#stopwatch)
   - [Table](#table)
   - [Textarea](#textarea)
   - [Textinput](#textinput)
   - [Timer](#timer)
   - [Viewport](#viewport)
4. [Light and Dark Styles](#4-light-and-dark-styles)
5. [Removed Symbols Reference](#5-removed-symbols-reference)

---

## 1. Import Paths

Replace all `github.com/charmbracelet/bubbles` imports with `charm.land/bubbles/v2`:

```go
// Before
import (
    "github.com/charmbracelet/bubbles/cursor"
    "github.com/charmbracelet/bubbles/help"
    "github.com/charmbracelet/bubbles/key"
    "github.com/charmbracelet/bubbles/list"
    "github.com/charmbracelet/bubbles/paginator"
    "github.com/charmbracelet/bubbles/progress"
    "github.com/charmbracelet/bubbles/runeutil"
    "github.com/charmbracelet/bubbles/spinner"
    "github.com/charmbracelet/bubbles/stopwatch"
    "github.com/charmbracelet/bubbles/table"
    "github.com/charmbracelet/bubbles/textarea"
    "github.com/charmbracelet/bubbles/textinput"
    "github.com/charmbracelet/bubbles/timer"
    "github.com/charmbracelet/bubbles/viewport"
)

// After
import (
    "charm.land/bubbles/v2/cursor"
    "charm.land/bubbles/v2/help"
    "charm.land/bubbles/v2/key"
    "charm.land/bubbles/v2/list"
    "charm.land/bubbles/v2/paginator"
    "charm.land/bubbles/v2/progress"
    "charm.land/bubbles/v2/spinner"
    "charm.land/bubbles/v2/stopwatch"
    "charm.land/bubbles/v2/table"
    "charm.land/bubbles/v2/textarea"
    "charm.land/bubbles/v2/textinput"
    "charm.land/bubbles/v2/timer"
    "charm.land/bubbles/v2/viewport"
)
```

> **Note:** The `runeutil` and `memoization` packages are now internal and no longer importable.

**Search-and-replace pattern:**

```
github.com/charmbracelet/bubbles/  →  charm.land/bubbles/v2/
github.com/charmbracelet/bubbles   →  charm.land/bubbles/v2
```

---

## 2. Global Patterns

These patterns repeat across multiple components. Address them first for the broadest impact.

### 2a. `tea.KeyMsg` → `tea.KeyPressMsg`

Bubble Tea v2 renames `tea.KeyMsg` to `tea.KeyPressMsg`. All Bubbles that handle key events have been updated. Update your own `Update` functions:

```go
// Before
case tea.KeyMsg:

// After
case tea.KeyPressMsg:
```

### 2b. Exported Width/Height Fields → Getter/Setter Methods

Many components replaced exported `Width` and `Height` fields with methods. The general pattern:

```go
// Before
m.Width = 40
m.Height = 20
fmt.Println(m.Width, m.Height)

// After
m.SetWidth(40)
m.SetHeight(20)
fmt.Println(m.Width(), m.Height())
```

**Affected components:** `filepicker`, `help`, `progress`, `table`, `textinput`, `viewport`.

### 2c. `DefaultKeyMap` Variables → Functions

Global mutable `DefaultKeyMap` variables are now functions returning fresh values:

```go
// Before
km := textinput.DefaultKeyMap
km.Paste.SetEnabled(false)

// After
km := textinput.DefaultKeyMap()
km.Paste.SetEnabled(false)
```

**Affected components:** `paginator`, `textarea`, `textinput`.

### 2d. `AdaptiveColor` → `LightDark` with `isDark bool`

Lip Gloss v2 removes `AdaptiveColor`. Style functions that previously auto-adapted now require an explicit `isDark bool` parameter. See [Section 4](#4-light-and-dark-styles) for the full pattern.

### 2e. Removed `NewModel` Aliases

All `NewModel` variables (deprecated aliases for `New`) have been removed. Use `New` directly.

**Affected components:** `help`, `list`, `paginator`, `spinner`, `textinput`.

---

## 3. Per-Component Migration

### Cursor

| v1 | v2 |
|----|-----|
| `model.Blink` | `model.IsBlinked` |
| `model.BlinkCmd()` | `model.Blink()` |

### Filepicker

| v1 | v2 |
|----|-----|
| `DefaultStylesWithRenderer(r)` | `DefaultStyles()` |
| `model.Height = 10` | `model.SetHeight(10)` |
| `_ = model.Height` | `_ = model.Height()` |

### Help

| v1 | v2 |
|----|-----|
| `model.Width = 80` | `model.SetWidth(80)` |
| `_ = model.Width` | `_ = model.Width()` |
| `NewModel()` | `New()` |

New functions:
- `DefaultStyles(isDark bool) Styles`
- `DefaultDarkStyles() Styles`
- `DefaultLightStyles() Styles`

Apply styles explicitly:

```go
// Before
h := help.New()
// Colors auto-adapted to terminal background

// After
h := help.New()
h.Styles = help.DefaultStyles(isDark)
```

### List

| v1 | v2 |
|----|-----|
| `DefaultStyles()` | `DefaultStyles(isDark)` |
| `NewDefaultItemStyles()` | `NewDefaultItemStyles(isDark)` |
| `styles.FilterPrompt` | `styles.Filter.Focused.Prompt` / `styles.Filter.Blurred.Prompt` |
| `styles.FilterCursor` | `styles.Filter.Cursor` |
| `NewModel(...)` | `New(...)` |

The `Styles.FilterPrompt` and `Styles.FilterCursor` fields have been consolidated into `Styles.Filter`, which is a `textinput.Styles` struct.

### Paginator

| v1 | v2 |
|----|-----|
| `DefaultKeyMap` (var) | `DefaultKeyMap()` (func) |
| `model.UsePgUpPgDownKeys` | Removed — customize `KeyMap` directly |
| `model.UseLeftRightKeys` | Removed — customize `KeyMap` directly |
| `model.UseUpDownKeys` | Removed — customize `KeyMap` directly |
| `model.UseHLKeys` | Removed — customize `KeyMap` directly |
| `model.UseJKKeys` | Removed — customize `KeyMap` directly |
| `NewModel(...)` | `New(...)` |

### Progress

This component has the most extensive changes.

#### Width

```go
// Before
p.Width = 40
fmt.Println(p.Width)

// After
p.SetWidth(40)
fmt.Println(p.Width())
```

#### Colors

Color types changed from `string` to `image/color.Color`:

```go
// Before
p.FullColor = "#FF0000"
p.EmptyColor = "#333333"

// After
p.FullColor = lipgloss.Color("#FF0000")
p.EmptyColor = lipgloss.Color("#333333")
```

#### Gradient/Blend Options

```go
// Before
progress.New(progress.WithGradient("#5A56E0", "#EE6FF8"))
progress.New(progress.WithDefaultGradient())
progress.New(progress.WithScaledGradient("#5A56E0", "#EE6FF8"))
progress.New(progress.WithDefaultScaledGradient())
progress.New(progress.WithSolidFill("#7571F9"))

// After
progress.New(progress.WithColors(lipgloss.Color("#5A56E0"), lipgloss.Color("#EE6FF8")))
progress.New(progress.WithDefaultBlend())
progress.New(progress.WithColors(lipgloss.Color("#5A56E0"), lipgloss.Color("#EE6FF8")), progress.WithScaled(true))
progress.New(progress.WithDefaultBlend(), progress.WithScaled(true))
progress.New(progress.WithColors(lipgloss.Color("#7571F9")))
```

| v1 | v2 |
|----|-----|
| `WithGradient(a, b string)` | `WithColors(colors ...color.Color)` |
| `WithDefaultGradient()` | `WithDefaultBlend()` |
| `WithScaledGradient(a, b string)` | `WithColors(...) + WithScaled(true)` |
| `WithDefaultScaledGradient()` | `WithDefaultBlend() + WithScaled(true)` |
| `WithSolidFill(string)` | `WithColors(color)` (single color) |
| `WithColorProfile(termenv.Profile)` | Removed (automatic) |
| `Update() (tea.Model, tea.Cmd)` | `Update() (Model, tea.Cmd)` |

New options:
- `WithColorFunc(func(total, current float64) color.Color)` — dynamic per-cell coloring
- `WithScaled(bool)` — scale blend to filled portion

### Spinner

| v1 | v2 |
|----|-----|
| `NewModel()` | `New()` |
| `spinner.Tick()` (package func) | `model.Tick()` (method) |

### Stopwatch

```go
// Before
sw := stopwatch.NewWithInterval(500 * time.Millisecond)

// After
sw := stopwatch.New(stopwatch.WithInterval(500 * time.Millisecond))
```

| v1 | v2 |
|----|-----|
| `NewWithInterval(d)` | `New(WithInterval(d))` |

### Table

| v1 | v2 |
|----|-----|
| `model.viewport.Width` | `model.Width()` / `model.SetWidth(w)` |
| `model.viewport.Height` | `model.Height()` / `model.SetHeight(h)` |

The table already had `SetWidth`/`SetHeight`/`Width()`/`Height()` in v1, but internally these now use viewport getter/setters.

### Textarea

#### KeyMap

```go
// Before
km := textarea.DefaultKeyMap
// After
km := textarea.DefaultKeyMap()
```

New key bindings added: `PageUp`, `PageDown`.

#### Styles

The styling system has been restructured:

```go
// Before
ta := textarea.New()
ta.FocusedStyle.Base = lipgloss.NewStyle().Border(lipgloss.RoundedBorder())
ta.BlurredStyle.Base = lipgloss.NewStyle().Border(lipgloss.HiddenBorder())

// After
ta := textarea.New()
// Styles are now nested under a Styles struct
// Access via Styles.Focused and Styles.Blurred (type StyleState)
```

| v1 | v2 |
|----|-----|
| `textarea.Style` (type) | `textarea.StyleState` (type) |
| `model.FocusedStyle` | `model.Styles.Focused` |
| `model.BlurredStyle` | `model.Styles.Blurred` |
| `DefaultStyles() (focused, blurred Style)` | `DefaultStyles(isDark bool) Styles` |

#### Cursor

```go
// Before
ta.Cursor                           // cursor.Model (virtual cursor)
ta.SetCursor(col)                   // set cursor column

// After
ta.Cursor()                         // func() *tea.Cursor (real cursor)
ta.SetCursorColumn(col)             // renamed for clarity
ta.VirtualCursor                    // bool: true = virtual, false = real
ta.Styles.Cursor                    // CursorStyle for cursor appearance
```

New additions:
- `Column()` — returns current cursor column (0-indexed)
- `ScrollYOffset()` — returns vertical scroll offset
- `ScrollPosition()` — returns scroll position
- `MoveToBeginning()` / `MoveToEnd()` — navigate to start/end

### Textinput

#### KeyMap

```go
// Before
km := textinput.DefaultKeyMap
// After
km := textinput.DefaultKeyMap()
```

#### Width

```go
// Before
ti.Width = 40
// After
ti.SetWidth(40)
```

#### Styles

Individual style fields have moved into a `Styles` struct:

```go
// Before
ti.PromptStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("63"))
ti.TextStyle = lipgloss.NewStyle()
ti.PlaceholderStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
ti.CompletionStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))

// After
s := textinput.DefaultStyles(isDark)
s.Focused.Prompt = lipgloss.NewStyle().Foreground(lipgloss.Color("63"))
s.Focused.Text = lipgloss.NewStyle()
s.Focused.Placeholder = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
s.Focused.Suggestion = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
ti.SetStyles(s)
```

| v1 Field | v2 Location |
|----------|-------------|
| `Model.PromptStyle` | `StyleState.Prompt` |
| `Model.TextStyle` | `StyleState.Text` |
| `Model.PlaceholderStyle` | `StyleState.Placeholder` |
| `Model.CompletionStyle` | `StyleState.Suggestion` |
| `Model.CursorStyle` | `Styles.Cursor` |
| `Model.Cursor` (cursor.Model) | `Model.Cursor()` (func → *tea.Cursor) |

New:
- `Model.Styles()` / `Model.SetStyles(Styles)` — get/set styles
- `Model.VirtualCursor()` / `Model.SetVirtualCursor(bool)` — toggle cursor mode

### Timer

```go
// Before
t := timer.NewWithInterval(30*time.Second, 100*time.Millisecond)
t := timer.New(30 * time.Second)

// After
t := timer.New(30*time.Second, timer.WithInterval(100*time.Millisecond))
t := timer.New(30 * time.Second)
```

| v1 | v2 |
|----|-----|
| `NewWithInterval(timeout, interval)` | `New(timeout, WithInterval(interval))` |

### Viewport

This component has the most new features alongside its breaking changes.

#### Constructor

```go
// Before
vp := viewport.New(80, 24)

// After
vp := viewport.New(viewport.WithWidth(80), viewport.WithHeight(24))
// or
vp := viewport.New()
vp.SetWidth(80)
vp.SetHeight(24)
```

#### Width, Height, YOffset

```go
// Before
vp.Width = 80
vp.Height = 24
vp.YOffset = 5
fmt.Println(vp.Width, vp.Height, vp.YOffset)

// After
vp.SetWidth(80)
vp.SetHeight(24)
vp.SetYOffset(5)
fmt.Println(vp.Width(), vp.Height(), vp.YOffset())
```

#### Removed

- `HighPerformanceRendering` — removed entirely (deprecated in Bubble Tea v2)

#### New Features (non-breaking)

These are additions you can adopt incrementally:

- **Soft wrapping:** `vp.SoftWrap = true`
- **Left gutter** for line numbers:
  ```go
  vp.LeftGutterFunc = func(info viewport.GutterContext) string {
      if info.Soft { return "     │ " }
      if info.Index >= info.TotalLines { return "   ~ │ " }
      return fmt.Sprintf("%4d │ ", info.Index+1)
  }
  ```
- **Highlighting:**
  ```go
  vp.SetHighlights(regexp.MustCompile("pattern").FindAllStringIndex(vp.GetContent(), -1))
  vp.HighlightNext()
  vp.HighlightPrevious()
  vp.ClearHighlights()
  ```
- **`SetContentLines([]string)`** — set lines directly with virtual soft-wrap support
- **`GetContent() string`** — retrieve content
- **`FillHeight bool`** — fill viewport with empty lines
- **`StyleLineFunc func(int) lipgloss.Style`** — per-line styling
- **Horizontal scrolling** with left/right arrow keys
- **Horizontal mouse wheel scrolling**

---

## 4. Light and Dark Styles

Lip Gloss v2 removes `AdaptiveColor`, so Bubbles no longer auto-detect terminal background. You must explicitly choose light or dark styles.

### Recommended: Query via Bubble Tea

```go
func (m model) Init() tea.Cmd {
    return tea.RequestBackgroundColor
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.BackgroundColorMsg:
        isDark := msg.IsDark()
        m.help.Styles = help.DefaultStyles(isDark)
        m.list.Styles = list.DefaultStyles(isDark)
        // ... apply to other components
    }
    return m, nil
}
```

This is required when using [Wish](https://github.com/charmbracelet/wish) to detect the client's background.

### Quick: Use `compat` Package

```go
import "charm.land/lipgloss/v2/compat"

var isDark = compat.HasDarkBackground()

func main() {
    h := help.New()
    h.Styles = help.DefaultStyles(isDark)
}
```

> **Warning:** The `compat` approach uses blocking I/O outside Bubble Tea's event loop and will not detect remote client backgrounds over SSH.

### Manual

```go
h.Styles = help.DefaultDarkStyles()   // force dark
h.Styles = help.DefaultLightStyles()  // force light
```

---

## 5. Removed Symbols Reference

Quick-reference table of all removed symbols and their replacements:

| Package | Removed | Replacement |
|---------|---------|-------------|
| `cursor` | `Model.Blink` | `Model.IsBlinked` |
| `cursor` | `Model.BlinkCmd()` | `Model.Blink()` |
| `filepicker` | `DefaultStylesWithRenderer(r)` | `DefaultStyles()` |
| `filepicker` | `Model.Height` (field) | `Model.SetHeight()` / `Model.Height()` |
| `help` | `NewModel` | `New()` |
| `help` | `Model.Width` (field) | `Model.SetWidth()` / `Model.Width()` |
| `list` | `NewModel` | `New()` |
| `list` | `DefaultStyles()` | `DefaultStyles(isDark)` |
| `list` | `NewDefaultItemStyles()` | `NewDefaultItemStyles(isDark)` |
| `list` | `Styles.FilterPrompt` | `Styles.Filter` (`textinput.Styles`) |
| `list` | `Styles.FilterCursor` | `Styles.Filter.Cursor` |
| `paginator` | `DefaultKeyMap` (var) | `DefaultKeyMap()` (func) |
| `paginator` | `NewModel` | `New()` |
| `paginator` | `UsePgUpPgDownKeys` etc. | Customize `KeyMap` directly |
| `progress` | `WithGradient(a, b)` | `WithColors(colors...)` |
| `progress` | `WithDefaultGradient()` | `WithDefaultBlend()` |
| `progress` | `WithScaledGradient(a, b)` | `WithColors(...) + WithScaled(true)` |
| `progress` | `WithDefaultScaledGradient()` | `WithDefaultBlend() + WithScaled(true)` |
| `progress` | `WithSolidFill(string)` | `WithColors(color)` |
| `progress` | `WithColorProfile(p)` | Removed (automatic) |
| `progress` | `Model.Width` (field) | `Model.SetWidth()` / `Model.Width()` |
| `spinner` | `NewModel` | `New()` |
| `spinner` | `Tick()` (package func) | `Model.Tick()` |
| `stopwatch` | `NewWithInterval(d)` | `New(WithInterval(d))` |
| `table` | `Model.Width` (field) | `Model.SetWidth()` / `Model.Width()` |
| `table` | `Model.Height` (field) | `Model.SetHeight()` / `Model.Height()` |
| `textarea` | `DefaultKeyMap` (var) | `DefaultKeyMap()` (func) |
| `textarea` | `Style` (type) | `StyleState` (type) |
| `textarea` | `Model.FocusedStyle` | `Model.Styles.Focused` |
| `textarea` | `Model.BlurredStyle` | `Model.Styles.Blurred` |
| `textarea` | `Model.SetCursor(col)` | `Model.SetCursorColumn(col)` |
| `textarea` | `DefaultStyles()` | `DefaultStyles(isDark)` |
| `textinput` | `DefaultKeyMap` (var) | `DefaultKeyMap()` (func) |
| `textinput` | `NewModel` | `New()` |
| `textinput` | `Model.Width` (field) | `Model.SetWidth()` / `Model.Width()` |
| `textinput` | `Model.PromptStyle` | `StyleState.Prompt` |
| `textinput` | `Model.TextStyle` | `StyleState.Text` |
| `textinput` | `Model.PlaceholderStyle` | `StyleState.Placeholder` |
| `textinput` | `Model.CompletionStyle` | `StyleState.Suggestion` |
| `textinput` | `Model.CursorStyle` | `Styles.Cursor` |
| `textinput` | `Model.Cursor` (cursor.Model) | `Model.Cursor()` (func → *tea.Cursor) |
| `timer` | `NewWithInterval(t, i)` | `New(t, WithInterval(i))` |
| `viewport` | `New(w, h int)` | `New(...Option)` |
| `viewport` | `Model.Width` (field) | `Model.SetWidth()` / `Model.Width()` |
| `viewport` | `Model.Height` (field) | `Model.SetHeight()` / `Model.Height()` |
| `viewport` | `Model.YOffset` (field) | `Model.SetYOffset()` / `Model.YOffset()` |
| `viewport` | `HighPerformanceRendering` | Removed |
| `runeutil` | Entire package | Moved to `internal/runeutil` (not importable) |

---

Part of [Charm](https://charm.land).

<a href="https://charm.land/"><img alt="The Charm logo" src="https://stuff.charm.sh/charm-badge.jpg" width="400"></a>


================================================
FILE: bubbles.go
================================================
// Package bubbles provides some components for Bubble Tea applications. These
// components are used in production in Glow, Charm and many other
// applications.
package bubbles


================================================
FILE: cursor/cursor.go
================================================
// Package cursor provides a virtual cursor to support the textinput and
// textarea elements.
package cursor

import (
	"context"
	"sync/atomic"
	"time"

	tea "charm.land/bubbletea/v2"
	"charm.land/lipgloss/v2"
)

const defaultBlinkSpeed = time.Millisecond * 530

// Internal ID management. Used during animating to ensure that frame messages
// are received only by spinner components that sent them.
var lastID int64

func nextID() int {
	return int(atomic.AddInt64(&lastID, 1))
}

// initialBlinkMsg initializes cursor blinking.
type initialBlinkMsg struct{}

// BlinkMsg signals that the cursor should blink. It contains metadata that
// allows us to tell if the blink message is the one we're expecting.
type BlinkMsg struct {
	id  int
	tag int
}

// blinkCanceled is sent when a blink operation is canceled.
type blinkCanceled struct{}

// blinkCtx manages cursor blinking.
type blinkCtx struct {
	ctx    context.Context
	cancel context.CancelFunc
}

// Mode describes the behavior of the cursor.
type Mode int

// Available cursor modes.
const (
	CursorBlink Mode = iota
	CursorStatic
	CursorHide
)

// String returns the cursor mode in a human-readable format. This method is
// provisional and for informational purposes only.
func (c Mode) String() string {
	return [...]string{
		"blink",
		"static",
		"hidden",
	}[c]
}

// Model is the Bubble Tea model for this cursor element.
type Model struct {
	// Style styles the cursor block.
	Style lipgloss.Style

	// TextStyle is the style used for the cursor when it is blinking
	// (hidden), i.e. displaying normal text.
	TextStyle lipgloss.Style

	// BlinkSpeed is the speed at which the cursor blinks. This has no effect
	// unless [CursorMode] is not set to [CursorBlink].
	BlinkSpeed time.Duration

	// IsBlinked is the state of the cursor blink. When true, the cursor is
	// hidden.
	IsBlinked bool

	// char is the character under the cursor
	char string

	// The ID of this Model as it relates to other cursors
	id int

	// focus indicates whether the containing input is focused
	focus bool

	// Used to manage cursor blink
	blinkCtx *blinkCtx

	// The ID of the blink message we're expecting to receive.
	blinkTag int

	// mode determines the behavior of the cursor
	mode Mode
}

// New creates a new model with default settings.
func New() Model {
	return Model{
		id:         nextID(),
		BlinkSpeed: defaultBlinkSpeed,
		IsBlinked:  true,
		mode:       CursorBlink,

		blinkCtx: &blinkCtx{
			ctx: context.Background(),
		},
	}
}

// Update updates the cursor.
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
	switch msg := msg.(type) {
	case initialBlinkMsg:
		// We accept all initialBlinkMsgs generated by the Blink command.

		if m.mode != CursorBlink || !m.focus {
			return m, nil
		}

		cmd := m.Blink()
		return m, cmd

	case tea.FocusMsg:
		return m, m.Focus()

	case tea.BlurMsg:
		m.Blur()
		return m, nil

	case BlinkMsg:
		// We're choosy about whether to accept blinkMsgs so that our cursor
		// only exactly when it should.

		// Is this model blink-able?
		if m.mode != CursorBlink || !m.focus {
			return m, nil
		}

		// Were we expecting this blink message?
		if msg.id != m.id || msg.tag != m.blinkTag {
			return m, nil
		}

		var cmd tea.Cmd
		if m.mode == CursorBlink {
			m.IsBlinked = !m.IsBlinked
			cmd = m.Blink()
		}
		return m, cmd

	case blinkCanceled: // no-op
		return m, nil
	}
	return m, nil
}

// Mode returns the model's cursor mode. For available cursor modes, see
// type Mode.
func (m Model) Mode() Mode {
	return m.mode
}

// SetMode sets the model's cursor mode. This method returns a command.
//
// For available cursor modes, see type CursorMode.
func (m *Model) SetMode(mode Mode) tea.Cmd {
	// Adjust the mode value if it's value is out of range
	if mode < CursorBlink || mode > CursorHide {
		return nil
	}
	m.mode = mode
	m.IsBlinked = m.mode == CursorHide || !m.focus
	if mode == CursorBlink {
		return Blink
	}
	return nil
}

// Blink is a command used to manage cursor blinking.
func (m *Model) Blink() tea.Cmd {
	if m.mode != CursorBlink {
		return nil
	}

	if m.blinkCtx != nil && m.blinkCtx.cancel != nil {
		m.blinkCtx.cancel()
	}

	ctx, cancel := context.WithTimeout(m.blinkCtx.ctx, m.BlinkSpeed)
	m.blinkCtx.cancel = cancel

	m.blinkTag++
	blinkMsg := BlinkMsg{id: m.id, tag: m.blinkTag}

	return func() tea.Msg {
		defer cancel()
		<-ctx.Done()
		if ctx.Err() == context.DeadlineExceeded {
			return blinkMsg
		}
		return blinkCanceled{}
	}
}

// Blink is a command used to initialize cursor blinking.
func Blink() tea.Msg {
	return initialBlinkMsg{}
}

// Focus focuses the cursor to allow it to blink if desired.
func (m *Model) Focus() tea.Cmd {
	m.focus = true
	m.IsBlinked = m.mode == CursorHide // show the cursor unless we've explicitly hidden it

	if m.mode == CursorBlink && m.focus {
		return m.Blink()
	}
	return nil
}

// Blur blurs the cursor.
func (m *Model) Blur() {
	m.focus = false
	m.IsBlinked = true
}

// SetChar sets the character under the cursor.
func (m *Model) SetChar(char string) {
	m.char = char
}

// View displays the cursor.
func (m Model) View() string {
	if m.IsBlinked {
		return m.TextStyle.Inline(true).Render(m.char)
	}
	return m.Style.Inline(true).Reverse(true).Render(m.char)
}


================================================
FILE: cursor/cursor_test.go
================================================
package cursor

import (
	"sync"
	"testing"
	"time"
)

// TestBlinkCmdDataRace tests for a race on [Cursor.blinkTag].
//
// The original [Model.Blink] implementation returned a closure over the pointer receiver:
//
//	return func() tea.Msg {
//		defer cancel()
//		<-ctx.Done()
//		if ctx.Err() == context.DeadlineExceeded {
//			return BlinkMsg{id: m.id, tag: m.blinkTag}
//		}
//		return blinkCanceled{}
//	}
//
// A race on “m.blinkTag” will occur if:
//  1. [Model.Blink] is called e.g. by calling [Model.Focus] from
//     ["charm.land/bubbletea/v2".Model.Update];
//  2. ["charm.land/bubbletea/v2".handleCommands] is kept sufficiently busy that it does not receive and
//     execute the [Model.BlinkCmd] e.g. by other long running command or commands;
//  3. at least [Mode.BlinkSpeed] time elapses;
//  4. [Model.Blink] is called again;
//  5. ["charm.land/bubbletea/v2".handleCommands] gets around to receiving and executing the original
//     closure.
//
// Even if this did not formally race, the value of the tag fetched would be semantically incorrect (likely being the
// current value rather than the value at the time the closure was created).
func TestBlinkCmdDataRace(t *testing.T) {
	m := New()
	cmd := m.Blink()
	var wg sync.WaitGroup
	wg.Add(2)
	go func() {
		defer wg.Done()
		time.Sleep(m.BlinkSpeed * 3)
		cmd()
	}()
	go func() {
		defer wg.Done()
		time.Sleep(m.BlinkSpeed * 2)
		m.Blink()
	}()
	wg.Wait()
}


================================================
FILE: filepicker/filepicker.go
================================================
// Package filepicker provides a file picker component for Bubble Tea
// applications.
package filepicker

import (
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
	"sync/atomic"

	"charm.land/bubbles/v2/key"
	tea "charm.land/bubbletea/v2"
	"charm.land/lipgloss/v2"
	"github.com/dustin/go-humanize"
)

var lastID int64

func nextID() int {
	return int(atomic.AddInt64(&lastID, 1))
}

// New returns a new filepicker model with default styling and key bindings.
func New() Model {
	return Model{
		id:               nextID(),
		CurrentDirectory: ".",
		Cursor:           ">",
		AllowedTypes:     []string{},
		selected:         0,
		ShowPermissions:  true,
		ShowSize:         true,
		ShowHidden:       false,
		DirAllowed:       false,
		FileAllowed:      true,
		AutoHeight:       true,
		height:           0,
		maxIdx:           0,
		minIdx:           0,
		selectedStack:    newStack(),
		minStack:         newStack(),
		maxStack:         newStack(),
		KeyMap:           DefaultKeyMap(),
		Styles:           DefaultStyles(),
	}
}

type errorMsg struct {
	err error
}

type readDirMsg struct {
	id      int
	entries []os.DirEntry
}

const (
	marginBottom  = 5
	fileSizeWidth = 7
	paddingLeft   = 2
)

// KeyMap defines key bindings for each user action.
type KeyMap struct {
	GoToTop  key.Binding
	GoToLast key.Binding
	Down     key.Binding
	Up       key.Binding
	PageUp   key.Binding
	PageDown key.Binding
	Back     key.Binding
	Open     key.Binding
	Select   key.Binding
}

// DefaultKeyMap defines the default keybindings.
func DefaultKeyMap() KeyMap {
	return KeyMap{
		GoToTop:  key.NewBinding(key.WithKeys("g"), key.WithHelp("g", "first")),
		GoToLast: key.NewBinding(key.WithKeys("G"), key.WithHelp("G", "last")),
		Down:     key.NewBinding(key.WithKeys("j", "down", "ctrl+n"), key.WithHelp("j", "down")),
		Up:       key.NewBinding(key.WithKeys("k", "up", "ctrl+p"), key.WithHelp("k", "up")),
		PageUp:   key.NewBinding(key.WithKeys("K", "pgup"), key.WithHelp("pgup", "page up")),
		PageDown: key.NewBinding(key.WithKeys("J", "pgdown"), key.WithHelp("pgdown", "page down")),
		Back:     key.NewBinding(key.WithKeys("h", "backspace", "left", "esc"), key.WithHelp("h", "back")),
		Open:     key.NewBinding(key.WithKeys("l", "right", "enter"), key.WithHelp("l", "open")),
		Select:   key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "select")),
	}
}

// Styles defines the possible customizations for styles in the file picker.
type Styles struct {
	DisabledCursor   lipgloss.Style
	Cursor           lipgloss.Style
	Symlink          lipgloss.Style
	Directory        lipgloss.Style
	File             lipgloss.Style
	DisabledFile     lipgloss.Style
	Permission       lipgloss.Style
	Selected         lipgloss.Style
	DisabledSelected lipgloss.Style
	FileSize         lipgloss.Style
	EmptyDirectory   lipgloss.Style
}

// DefaultStyles defines the default styling for the file picker.
func DefaultStyles() Styles {
	return Styles{
		DisabledCursor:   lipgloss.NewStyle().Foreground(lipgloss.Color("247")),
		Cursor:           lipgloss.NewStyle().Foreground(lipgloss.Color("212")),
		Symlink:          lipgloss.NewStyle().Foreground(lipgloss.Color("36")),
		Directory:        lipgloss.NewStyle().Foreground(lipgloss.Color("99")),
		File:             lipgloss.NewStyle(),
		DisabledFile:     lipgloss.NewStyle().Foreground(lipgloss.Color("243")),
		DisabledSelected: lipgloss.NewStyle().Foreground(lipgloss.Color("247")),
		Permission:       lipgloss.NewStyle().Foreground(lipgloss.Color("244")),
		Selected:         lipgloss.NewStyle().Foreground(lipgloss.Color("212")).Bold(true),
		FileSize:         lipgloss.NewStyle().Foreground(lipgloss.Color("240")).Width(fileSizeWidth).Align(lipgloss.Right),
		EmptyDirectory:   lipgloss.NewStyle().Foreground(lipgloss.Color("240")).PaddingLeft(paddingLeft).SetString("Bummer. No Files Found."),
	}
}

// Model represents a file picker.
type Model struct {
	id int

	// Path is the path which the user has selected with the file picker.
	Path string

	// CurrentDirectory is the directory that the user is currently in.
	CurrentDirectory string

	// AllowedTypes specifies which file types the user may select.
	// If empty the user may select any file.
	AllowedTypes []string

	KeyMap          KeyMap
	files           []os.DirEntry
	ShowPermissions bool
	ShowSize        bool
	ShowHidden      bool
	DirAllowed      bool
	FileAllowed     bool

	FileSelected  string
	selected      int
	selectedStack stack

	minIdx   int
	maxIdx   int
	maxStack stack
	minStack stack

	height     int
	AutoHeight bool

	Cursor string
	Styles Styles
}

type stack struct {
	Push   func(int)
	Pop    func() int
	Length func() int
}

func newStack() stack {
	slice := make([]int, 0)
	return stack{
		Push: func(i int) {
			slice = append(slice, i)
		},
		Pop: func() int {
			res := slice[len(slice)-1]
			slice = slice[:len(slice)-1]
			return res
		},
		Length: func() int {
			return len(slice)
		},
	}
}

func (m *Model) pushView(selected, minimum, maximum int) {
	m.selectedStack.Push(selected)
	m.minStack.Push(minimum)
	m.maxStack.Push(maximum)
}

func (m *Model) popView() (int, int, int) {
	return m.selectedStack.Pop(), m.minStack.Pop(), m.maxStack.Pop()
}

func (m Model) readDir(path string, showHidden bool) tea.Cmd {
	return func() tea.Msg {
		dirEntries, err := os.ReadDir(path)
		if err != nil {
			return errorMsg{err}
		}

		sort.Slice(dirEntries, func(i, j int) bool {
			if dirEntries[i].IsDir() == dirEntries[j].IsDir() {
				return dirEntries[i].Name() < dirEntries[j].Name()
			}
			return dirEntries[i].IsDir()
		})

		if showHidden {
			return readDirMsg{id: m.id, entries: dirEntries}
		}

		var sanitizedDirEntries []os.DirEntry
		for _, dirEntry := range dirEntries {
			isHidden, _ := IsHidden(dirEntry.Name())
			if isHidden {
				continue
			}
			sanitizedDirEntries = append(sanitizedDirEntries, dirEntry)
		}
		return readDirMsg{id: m.id, entries: sanitizedDirEntries}
	}
}

// SetHeight sets the height of the file picker.
func (m *Model) SetHeight(h int) {
	m.height = h
	if m.maxIdx > m.height-1 {
		m.maxIdx = m.minIdx + m.height - 1
	}
}

// Height returns the height of the file picker.
func (m Model) Height() int {
	return m.height
}

// Init initializes the file picker model.
func (m Model) Init() tea.Cmd {
	return m.readDir(m.CurrentDirectory, m.ShowHidden)
}

// Update handles user interactions within the file picker model.
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
	switch msg := msg.(type) {
	case readDirMsg:
		if msg.id != m.id {
			break
		}
		m.files = msg.entries
		m.maxIdx = max(m.maxIdx, m.Height()-1)
	case tea.WindowSizeMsg:
		if m.AutoHeight {
			m.SetHeight(msg.Height - marginBottom)
		}
		m.maxIdx = m.Height() - 1
	case tea.KeyPressMsg:
		switch {
		case key.Matches(msg, m.KeyMap.GoToTop):
			m.selected = 0
			m.minIdx = 0
			m.maxIdx = m.Height() - 1
		case key.Matches(msg, m.KeyMap.GoToLast):
			m.selected = len(m.files) - 1
			m.minIdx = len(m.files) - m.Height()
			m.maxIdx = len(m.files) - 1
		case key.Matches(msg, m.KeyMap.Down):
			m.selected++
			if m.selected >= len(m.files) {
				m.selected = len(m.files) - 1
			}
			if m.selected > m.maxIdx {
				m.minIdx++
				m.maxIdx++
			}
		case key.Matches(msg, m.KeyMap.Up):
			m.selected--
			if m.selected < 0 {
				m.selected = 0
			}
			if m.selected < m.minIdx {
				m.minIdx--
				m.maxIdx--
			}
		case key.Matches(msg, m.KeyMap.PageDown):
			m.selected += m.Height()
			if m.selected >= len(m.files) {
				m.selected = len(m.files) - 1
			}
			m.minIdx += m.Height()
			m.maxIdx += m.Height()

			if m.maxIdx >= len(m.files) {
				m.maxIdx = len(m.files) - 1
				m.minIdx = m.maxIdx - m.Height()
			}
		case key.Matches(msg, m.KeyMap.PageUp):
			m.selected -= m.Height()
			if m.selected < 0 {
				m.selected = 0
			}
			m.minIdx -= m.Height()
			m.maxIdx -= m.Height()

			if m.minIdx < 0 {
				m.minIdx = 0
				m.maxIdx = m.minIdx + m.Height()
			}
		case key.Matches(msg, m.KeyMap.Back):
			m.CurrentDirectory = filepath.Dir(m.CurrentDirectory)
			if m.selectedStack.Length() > 0 {
				m.selected, m.minIdx, m.maxIdx = m.popView()
			} else {
				m.selected = 0
				m.minIdx = 0
				m.maxIdx = m.Height() - 1
			}
			return m, m.readDir(m.CurrentDirectory, m.ShowHidden)
		case key.Matches(msg, m.KeyMap.Open):
			if len(m.files) == 0 {
				break
			}

			f := m.files[m.selected]
			info, err := f.Info()
			if err != nil {
				break
			}
			isSymlink := info.Mode()&os.ModeSymlink != 0
			isDir := f.IsDir()

			if isSymlink {
				symlinkPath, _ := filepath.EvalSymlinks(filepath.Join(m.CurrentDirectory, f.Name()))
				info, err := os.Stat(symlinkPath)
				if err != nil {
					break
				}
				if info.IsDir() {
					isDir = true
				}
			}

			if (!isDir && m.FileAllowed) || (isDir && m.DirAllowed) {
				if key.Matches(msg, m.KeyMap.Select) {
					// Select the current path as the selection
					m.Path = filepath.Join(m.CurrentDirectory, f.Name())
				}
			}

			if !isDir {
				break
			}

			m.CurrentDirectory = filepath.Join(m.CurrentDirectory, f.Name())
			m.pushView(m.selected, m.minIdx, m.maxIdx)
			m.selected = 0
			m.minIdx = 0
			m.maxIdx = m.Height() - 1
			return m, m.readDir(m.CurrentDirectory, m.ShowHidden)
		}
	}
	return m, nil
}

// View returns the view of the file picker.
func (m Model) View() string {
	if len(m.files) == 0 {
		return m.Styles.EmptyDirectory.Height(m.Height()).MaxHeight(m.Height()).String()
	}
	var s strings.Builder

	for i, f := range m.files {
		if i < m.minIdx || i > m.maxIdx {
			continue
		}

		var symlinkPath string
		info, err := f.Info()
		if err != nil {
			continue
		}
		isSymlink := info.Mode()&os.ModeSymlink != 0
		size := strings.Replace(humanize.Bytes(uint64(info.Size())), " ", "", 1) //nolint:gosec
		name := f.Name()

		if isSymlink {
			symlinkPath, _ = filepath.EvalSymlinks(filepath.Join(m.CurrentDirectory, name))
		}

		disabled := !m.canSelect(name) && !f.IsDir()

		if m.selected == i { //nolint:nestif
			selected := ""
			if m.ShowPermissions {
				selected += " " + info.Mode().String()
			}
			if m.ShowSize {
				selected += fmt.Sprintf("%"+strconv.Itoa(m.Styles.FileSize.GetWidth())+"s", size)
			}
			selected += " " + name
			if isSymlink {
				selected += " → " + symlinkPath
			}
			if disabled {
				s.WriteString(m.Styles.DisabledCursor.Render(m.Cursor) + m.Styles.DisabledSelected.Render(selected))
			} else {
				s.WriteString(m.Styles.Cursor.Render(m.Cursor) + m.Styles.Selected.Render(selected))
			}
			s.WriteRune('\n')
			continue
		}

		style := m.Styles.File
		if f.IsDir() {
			style = m.Styles.Directory
		} else if isSymlink {
			style = m.Styles.Symlink
		} else if disabled {
			style = m.Styles.DisabledFile
		}

		fileName := style.Render(name)
		s.WriteString(m.Styles.Cursor.Render(" "))
		if isSymlink {
			fileName += " → " + symlinkPath
		}
		if m.ShowPermissions {
			s.WriteString(" " + m.Styles.Permission.Render(info.Mode().String()))
		}
		if m.ShowSize {
			s.WriteString(m.Styles.FileSize.Render(size))
		}
		s.WriteString(" " + fileName)
		s.WriteRune('\n')
	}

	for i := lipgloss.Height(s.String()); i <= m.Height(); i++ {
		s.WriteRune('\n')
	}

	return s.String()
}

// DidSelectFile returns whether a user has selected a file (on this msg).
func (m Model) DidSelectFile(msg tea.Msg) (bool, string) {
	didSelect, path := m.didSelectFile(msg)
	if didSelect && m.canSelect(path) {
		return true, path
	}
	return false, ""
}

// DidSelectDisabledFile returns whether a user tried to select a disabled file
// (on this msg). This is necessary only if you would like to warn the user that
// they tried to select a disabled file.
func (m Model) DidSelectDisabledFile(msg tea.Msg) (bool, string) {
	didSelect, path := m.didSelectFile(msg)
	if didSelect && !m.canSelect(path) {
		return true, path
	}
	return false, ""
}

func (m Model) didSelectFile(msg tea.Msg) (bool, string) {
	if len(m.files) == 0 {
		return false, ""
	}
	switch msg := msg.(type) {
	case tea.KeyPressMsg:
		// If the msg does not match the Select keymap then this could not have been a selection.
		if !key.Matches(msg, m.KeyMap.Select) {
			return false, ""
		}

		// The key press was a selection, let's confirm whether the current file could
		// be selected or used for navigating deeper into the stack.
		f := m.files[m.selected]
		info, err := f.Info()
		if err != nil {
			return false, ""
		}
		isSymlink := info.Mode()&os.ModeSymlink != 0
		isDir := f.IsDir()

		if isSymlink {
			symlinkPath, _ := filepath.EvalSymlinks(filepath.Join(m.CurrentDirectory, f.Name()))
			info, err := os.Stat(symlinkPath)
			if err != nil {
				break
			}
			if info.IsDir() {
				isDir = true
			}
		}

		if (!isDir && m.FileAllowed) || (isDir && m.DirAllowed) && m.Path != "" {
			return true, m.Path
		}

		// If the msg was not a KeyPressMsg, then the file could not have been selected this iteration.
		// Only a KeyPressMsg can select a file.
	default:
		return false, ""
	}
	return false, ""
}

func (m Model) canSelect(file string) bool {
	if len(m.AllowedTypes) <= 0 {
		return true
	}

	for _, ext := range m.AllowedTypes {
		if strings.HasSuffix(file, ext) {
			return true
		}
	}
	return false
}

// HighlightedPath returns the path of the currently highlighted file or directory.
func (m Model) HighlightedPath() string {
	if len(m.files) == 0 || m.selected < 0 || m.selected >= len(m.files) {
		return ""
	}
	return filepath.Join(m.CurrentDirectory, m.files[m.selected].Name())
}


================================================
FILE: filepicker/hidden_unix.go
================================================
//go:build !windows
// +build !windows

package filepicker

import "strings"

// IsHidden reports whether a file is hidden or not.
func IsHidden(file string) (bool, error) {
	return strings.HasPrefix(file, "."), nil
}


================================================
FILE: filepicker/hidden_windows.go
================================================
//go:build windows
// +build windows

package filepicker

import (
	"syscall"
)

// IsHidden reports whether a file is hidden or not.
func IsHidden(file string) (bool, error) {
	pointer, err := syscall.UTF16PtrFromString(file)
	if err != nil {
		return false, err //nolint:wrapcheck
	}
	attributes, err := syscall.GetFileAttributes(pointer)
	if err != nil {
		return false, err //nolint:wrapcheck
	}
	return attributes&syscall.FILE_ATTRIBUTE_HIDDEN != 0, nil
}


================================================
FILE: go.mod
================================================
module charm.land/bubbles/v2

go 1.25.0

require (
	charm.land/bubbletea/v2 v2.0.2
	charm.land/lipgloss/v2 v2.0.2
	github.com/MakeNowJust/heredoc v1.0.0
	github.com/atotto/clipboard v0.1.4
	github.com/charmbracelet/harmonica v0.2.0
	github.com/charmbracelet/x/ansi v0.11.6
	github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f
	github.com/dustin/go-humanize v1.0.1
	github.com/mattn/go-runewidth v0.0.21
	github.com/rivo/uniseg v0.4.7
	github.com/sahilm/fuzzy v0.1.1
)

require (
	github.com/aymanbagabas/go-udiff v0.4.1 // indirect
	github.com/charmbracelet/colorprofile v0.4.2 // indirect
	github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect
	github.com/charmbracelet/x/term v0.2.2 // indirect
	github.com/charmbracelet/x/termios v0.1.1 // indirect
	github.com/charmbracelet/x/windows v0.2.2 // indirect
	github.com/clipperhouse/displaywidth v0.11.0 // indirect
	github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
	github.com/kylelemons/godebug v1.1.0 // indirect
	github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
	github.com/muesli/cancelreader v0.2.2 // indirect
	github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
	golang.org/x/sync v0.19.0 // indirect
	golang.org/x/sys v0.42.0 // indirect
)


================================================
FILE: go.sum
================================================
charm.land/bubbletea/v2 v2.0.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0=
charm.land/bubbletea/v2 v2.0.2/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ=
charm.land/lipgloss/v2 v2.0.2 h1:xFolbF8JdpNkM2cEPTfXEcW1p6NRzOWTSamRfYEw8cs=
charm.land/lipgloss/v2 v2.0.2/go.mod h1:KjPle2Qd3YmvP1KL5OMHiHysGcNwq6u83MUjYkFvEkM=
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=
github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY=
github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8=
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 h1:eyFRbAmexyt43hVfeyBofiGSEmJ7krjLOYt/9CF5NKA=
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8/go.mod h1:SQpCTRNBtzJkwku5ye4S3HEuthAlGy2n9VXZnWkEW98=
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=
github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w=
github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=


================================================
FILE: help/help.go
================================================
// Package help provides a simple help view for Bubble Tea applications.
package help

import (
	"strings"

	"charm.land/bubbles/v2/key"
	tea "charm.land/bubbletea/v2"
	"charm.land/lipgloss/v2"
)

// KeyMap is a map of keybindings used to generate help. Since it's an
// interface it can be any type, though struct or a map[string][]key.Binding
// are likely candidates.
//
// Note that if a key is disabled (via key.Binding.SetEnabled) it will not be
// rendered in the help view, so in theory generated help should self-manage.
type KeyMap interface {
	// ShortHelp returns a slice of bindings to be displayed in the short
	// version of the help. The help bubble will render help in the order in
	// which the help items are returned here.
	ShortHelp() []key.Binding

	// FullHelp returns an extended group of help items, grouped by columns.
	// The help bubble will render the help in the order in which the help
	// items are returned here.
	FullHelp() [][]key.Binding
}

// Styles is a set of available style definitions for the Help bubble.
type Styles struct {
	Ellipsis lipgloss.Style

	// Styling for the short help
	ShortKey       lipgloss.Style
	ShortDesc      lipgloss.Style
	ShortSeparator lipgloss.Style

	// Styling for the full help
	FullKey       lipgloss.Style
	FullDesc      lipgloss.Style
	FullSeparator lipgloss.Style
}

// DefaultStyles returns a set of default styles for the help bubble. Light or
// dark styles can be selected by passing true or false to the isDark
// parameter.
func DefaultStyles(isDark bool) Styles {
	lightDark := lipgloss.LightDark(isDark)

	keyStyle := lipgloss.NewStyle().Foreground(lightDark(lipgloss.Color("#909090"), lipgloss.Color("#626262")))
	descStyle := lipgloss.NewStyle().Foreground(lightDark(lipgloss.Color("#B2B2B2"), lipgloss.Color("#4A4A4A")))
	sepStyle := lipgloss.NewStyle().Foreground(lightDark(lipgloss.Color("#DADADA"), lipgloss.Color("#3C3C3C")))

	return Styles{
		ShortKey:       keyStyle,
		ShortDesc:      descStyle,
		ShortSeparator: sepStyle,
		Ellipsis:       sepStyle,
		FullKey:        keyStyle,
		FullDesc:       descStyle,
		FullSeparator:  sepStyle,
	}
}

// DefaultDarkStyles returns a set of default styles for dark backgrounds.
func DefaultDarkStyles() Styles {
	return DefaultStyles(true)
}

// DefaultLightStyles returns a set of default styles for light backgrounds.
func DefaultLightStyles() Styles {
	return DefaultStyles(false)
}

// Model contains the state of the help view.
type Model struct {
	ShowAll bool // if true, render the "full" help menu

	ShortSeparator string
	FullSeparator  string

	// The symbol we use in the short help when help items have been truncated
	// due to width. Periods of ellipsis by default.
	Ellipsis string

	Styles Styles

	width int
}

// New creates a new help view with some useful defaults.
func New() Model {
	return Model{
		ShortSeparator: " • ",
		FullSeparator:  "    ",
		Ellipsis:       "…",
		Styles:         DefaultDarkStyles(),
	}
}

// Update helps satisfy the Bubble Tea Model interface. It's a no-op.
func (m Model) Update(_ tea.Msg) (Model, tea.Cmd) {
	return m, nil
}

// View renders the help view's current state.
func (m Model) View(k KeyMap) string {
	if m.ShowAll {
		return m.FullHelpView(k.FullHelp())
	}
	return m.ShortHelpView(k.ShortHelp())
}

// SetWidth sets the maximum width for the help view.
func (m *Model) SetWidth(w int) {
	m.width = w
}

// Width returns the maximum width for the help view.
func (m Model) Width() int {
	return m.width
}

// ShortHelpView renders a single line help view from a slice of keybindings.
// If the line is longer than the maximum width it will be gracefully
// truncated, showing only as many help items as possible.
func (m Model) ShortHelpView(bindings []key.Binding) string {
	if len(bindings) == 0 {
		return ""
	}

	var b strings.Builder
	var totalWidth int
	separator := m.Styles.ShortSeparator.Inline(true).Render(m.ShortSeparator)

	for i, kb := range bindings {
		if !kb.Enabled() {
			continue
		}

		// Sep
		var sep string
		if totalWidth > 0 && i < len(bindings) {
			sep = separator
		}

		// Item
		str := sep +
			m.Styles.ShortKey.Inline(true).Render(kb.Help().Key) + " " +
			m.Styles.ShortDesc.Inline(true).Render(kb.Help().Desc)
		w := lipgloss.Width(str)

		// Tail
		if tail, ok := m.shouldAddItem(totalWidth, w); !ok {
			if tail != "" {
				b.WriteString(tail)
			}
			break
		}

		totalWidth += w
		b.WriteString(str)
	}

	return b.String()
}

// FullHelpView renders help columns from a slice of key binding slices. Each
// top level slice entry renders into a column.
func (m Model) FullHelpView(groups [][]key.Binding) string {
	if len(groups) == 0 {
		return ""
	}

	// Linter note: at this time we don't think it's worth the additional
	// code complexity involved in preallocating this slice.
	var (
		out []string

		totalWidth int
		separator  = m.Styles.FullSeparator.Inline(true).Render(m.FullSeparator)
	)

	// Iterate over groups to build columns
	for i, group := range groups {
		if group == nil || !shouldRenderColumn(group) {
			continue
		}
		var (
			sep          string
			keys         []string
			descriptions []string
		)

		// Sep
		if totalWidth > 0 && i < len(groups) {
			sep = separator
		}

		// Separate keys and descriptions into different slices
		for _, kb := range group {
			if !kb.Enabled() {
				continue
			}
			keys = append(keys, kb.Help().Key)
			descriptions = append(descriptions, kb.Help().Desc)
		}

		// Column
		col := lipgloss.JoinHorizontal(lipgloss.Top,
			sep,
			m.Styles.FullKey.Render(strings.Join(keys, "\n")),
			" ",
			m.Styles.FullDesc.Render(strings.Join(descriptions, "\n")),
		)
		w := lipgloss.Width(col)

		// Tail
		if tail, ok := m.shouldAddItem(totalWidth, w); !ok {
			if tail != "" {
				out = append(out, tail)
			}
			break
		}

		totalWidth += w
		out = append(out, col)
	}

	return lipgloss.JoinHorizontal(lipgloss.Top, out...)
}

func (m Model) shouldAddItem(totalWidth, width int) (tail string, ok bool) {
	// If there's room for an ellipsis, print that.
	if m.width > 0 && totalWidth+width > m.width {
		tail = " " + m.Styles.Ellipsis.Inline(true).Render(m.Ellipsis)

		if totalWidth+lipgloss.Width(tail) < m.width {
			return tail, false
		}
	}
	return "", true
}

func shouldRenderColumn(b []key.Binding) (ok bool) {
	for _, v := range b {
		if v.Enabled() {
			return true
		}
	}
	return false
}


================================================
FILE: help/help_test.go
================================================
package help

import (
	"fmt"
	"testing"

	"charm.land/bubbles/v2/key"
	"github.com/charmbracelet/x/ansi"
	"github.com/charmbracelet/x/exp/golden"
)

func TestFullHelp(t *testing.T) {
	m := New()
	m.FullSeparator = " | "
	k := key.WithKeys("x")
	kb := [][]key.Binding{
		{
			key.NewBinding(k, key.WithHelp("enter", "continue")),
		},
		{
			key.NewBinding(k, key.WithHelp("esc", "back")),
			key.NewBinding(k, key.WithHelp("?", "help")),
		},
		{
			key.NewBinding(k, key.WithHelp("H", "home")),
			key.NewBinding(k, key.WithHelp("ctrl+c", "quit")),
			key.NewBinding(k, key.WithHelp("ctrl+l", "log")),
		},
	}

	for _, w := range []int{20, 30, 40} {
		t.Run(fmt.Sprintf("full help %d width", w), func(t *testing.T) {
			m.SetWidth(w)
			s := m.FullHelpView(kb)
			s = ansi.Strip(s)
			golden.RequireEqual(t, []byte(s))
		})
	}
}


================================================
FILE: help/testdata/TestFullHelp/full_help_20_width.golden
================================================
enter continue …

================================================
FILE: help/testdata/TestFullHelp/full_help_30_width.golden
================================================
enter continue | esc back …
                 ?   help  

================================================
FILE: help/testdata/TestFullHelp/full_help_40_width.golden
================================================
enter continue | esc back | H      home
                 ?   help   ctrl+c quit
                            ctrl+l log 

================================================
FILE: internal/memoization/memoization.go
================================================
// Package memoization implement a simple memoization cache. It's designed to
// improve performance in textarea.
package memoization

import (
	"container/list"
	"crypto/sha256"
	"fmt"
	"sync"
)

// Hasher is an interface that requires a Hash method. The Hash method is
// expected to return a string representation of the hash of the object.
type Hasher interface {
	Hash() string
}

// entry is a struct that holds a key-value pair. It is used as an element
// in the evictionList of the MemoCache.
type entry[T any] struct {
	key   string
	value T
}

// MemoCache is a struct that represents a cache with a set capacity. It
// uses an LRU (Least Recently Used) eviction policy. It is safe for
// concurrent use.
type MemoCache[H Hasher, T any] struct {
	capacity      int
	mutex         sync.Mutex
	cache         map[string]*list.Element // The cache holding the results
	evictionList  *list.List               // A list to keep track of the order for LRU
	hashableItems map[string]T             // This map keeps track of the original hashable items (optional)
}

// NewMemoCache is a function that creates a new MemoCache with a given
// capacity. It returns a pointer to the created MemoCache.
func NewMemoCache[H Hasher, T any](capacity int) *MemoCache[H, T] {
	return &MemoCache[H, T]{
		capacity:      capacity,
		cache:         make(map[string]*list.Element),
		evictionList:  list.New(),
		hashableItems: make(map[string]T),
	}
}

// Capacity is a method that returns the capacity of the MemoCache.
func (m *MemoCache[H, T]) Capacity() int {
	return m.capacity
}

// Size is a method that returns the current size of the MemoCache. It is
// the number of items currently stored in the cache.
func (m *MemoCache[H, T]) Size() int {
	m.mutex.Lock()
	defer m.mutex.Unlock()
	return m.evictionList.Len()
}

// Get is a method that returns the value associated with the given
// hashable item in the MemoCache. If there is no corresponding value, the
// method returns nil.
func (m *MemoCache[H, T]) Get(h H) (T, bool) {
	m.mutex.Lock()
	defer m.mutex.Unlock()

	hashedKey := h.Hash()
	if element, found := m.cache[hashedKey]; found {
		m.evictionList.MoveToFront(element)
		return element.Value.(*entry[T]).value, true
	}
	var result T
	return result, false
}

// Set is a method that sets the value for the given hashable item in the
// MemoCache. If the cache is at capacity, it evicts the least recently
// used item before adding the new item.
func (m *MemoCache[H, T]) Set(h H, value T) {
	m.mutex.Lock()
	defer m.mutex.Unlock()

	hashedKey := h.Hash()
	if element, found := m.cache[hashedKey]; found {
		m.evictionList.MoveToFront(element)
		element.Value.(*entry[T]).value = value
		return
	}

	// Check if the cache is at capacity
	if m.evictionList.Len() >= m.capacity {
		// Evict the least recently used item from the cache
		toEvict := m.evictionList.Back()
		if toEvict != nil {
			evictedEntry := m.evictionList.Remove(toEvict).(*entry[T])
			delete(m.cache, evictedEntry.key)
			delete(m.hashableItems, evictedEntry.key) // if you're keeping track of original items
		}
	}

	// Add the value to the cache and the evictionList
	newEntry := &entry[T]{
		key:   hashedKey,
		value: value,
	}
	element := m.evictionList.PushFront(newEntry)
	m.cache[hashedKey] = element
	m.hashableItems[hashedKey] = value // if you're keeping track of original items
}

// HString is a type that implements the Hasher interface for strings.
type HString string

// Hash is a method that returns the hash of the string.
func (h HString) Hash() string {
	return fmt.Sprintf("%x", sha256.Sum256([]byte(h)))
}

// HInt is a type that implements the Hasher interface for integers.
type HInt int

// Hash is a method that returns the hash of the integer.
func (h HInt) Hash() string {
	return fmt.Sprintf("%x", sha256.Sum256(fmt.Appendf(nil, "%d", h)))
}


================================================
FILE: internal/memoization/memoization_test.go
================================================
package memoization

import (
	"encoding/binary"
	"fmt"
	"os"
	"slices"
	"testing"
)

type actionType int

const (
	set actionType = iota
	get
)

type cacheAction struct {
	actionType    actionType
	key           HString
	value         any
	expectedValue any
}

type testCase struct {
	name     string
	capacity int
	actions  []cacheAction
}

func TestCache(t *testing.T) {
	tests := []testCase{
		{
			name:     "TestNewMemoCache",
			capacity: 5,
			actions: []cacheAction{
				{actionType: get, expectedValue: nil},
			},
		},
		{
			name:     "TestSetAndGet",
			capacity: 10,
			actions: []cacheAction{
				{actionType: set, key: "key1", value: "value1"},
				{actionType: get, key: "key1", expectedValue: "value1"},
				{actionType: set, key: "key1", value: "newValue1"},
				{actionType: get, key: "key1", expectedValue: "newValue1"},
				{actionType: get, key: "nonExistentKey", expectedValue: nil},
				{actionType: set, key: "nilKey", value: ""},
				{actionType: get, key: "nilKey", expectedValue: ""},
				{actionType: set, key: "keyA", value: "valueA"},
				{actionType: set, key: "keyB", value: "valueB"},
				{actionType: get, key: "keyA", expectedValue: "valueA"},
				{actionType: get, key: "keyB", expectedValue: "valueB"},
			},
		},
		{
			name:     "TestSetNilValue",
			capacity: 10,
			actions: []cacheAction{
				{actionType: set, key: HString("nilKey"), value: nil},
				{actionType: get, key: HString("nilKey"), expectedValue: nil},
			},
		},
		{
			name:     "TestGetAfterEviction",
			capacity: 2,
			actions: []cacheAction{
				{actionType: set, key: HString("1"), value: 1},
				{actionType: set, key: HString("2"), value: 2},
				{actionType: set, key: HString("3"), value: 3},
				{actionType: get, key: HString("1"), expectedValue: nil},
				{actionType: get, key: HString("2"), expectedValue: 2},
			},
		},
		{
			name:     "TestGetAfterLRU",
			capacity: 2,
			actions: []cacheAction{
				{actionType: set, key: HString("1"), value: 1},
				{actionType: set, key: HString("2"), value: 2},
				{actionType: get, key: HString("1"), expectedValue: 1},
				{actionType: set, key: HString("3"), value: 3},
				{actionType: get, key: HString("1"), expectedValue: 1},
				{actionType: get, key: HString("3"), expectedValue: 3},
				{actionType: get, key: HString("2"), expectedValue: nil},
			},
		},
		{
			name:     "TestLRU_Capacity3",
			capacity: 3,
			actions: []cacheAction{
				{actionType: set, key: HString("1"), value: 1},
				{actionType: set, key: HString("2"), value: 2},
				{actionType: set, key: HString("3"), value: 3},
				{actionType: get, key: HString("1"), expectedValue: 1}, // Accessing key "1"
				{actionType: set, key: HString("4"), value: 4},         // Should evict key "2" since "1" was recently accessed
				{actionType: get, key: HString("2"), expectedValue: nil},
				{actionType: get, key: HString("1"), expectedValue: 1},
				{actionType: get, key: HString("3"), expectedValue: 3},
				{actionType: get, key: HString("4"), expectedValue: 4},
			},
		},
		// Test LRU behavior with varying accesses
		{
			name:     "TestLRU_VaryingAccesses",
			capacity: 3,
			actions: []cacheAction{
				{actionType: set, key: HString("1"), value: 1},
				{actionType: set, key: HString("2"), value: 2},
				{actionType: set, key: HString("3"), value: 3},
				{actionType: get, key: HString("1"), expectedValue: 1}, // Accessing key "1"
				{actionType: get, key: HString("2"), expectedValue: 2}, // Accessing key "2"
				{actionType: set, key: HString("4"), value: 4},         // Should evict key "3"
				{actionType: get, key: HString("3"), expectedValue: nil},
				{actionType: get, key: HString("1"), expectedValue: 1},
				{actionType: get, key: HString("2"), expectedValue: 2},
				{actionType: get, key: HString("4"), expectedValue: 4},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cache := NewMemoCache[HString, any](tt.capacity)
			for _, action := range tt.actions {
				switch action.actionType {
				case set:
					cache.Set(action.key, action.value)
				case get:
					if got, _ := cache.Get(action.key); got != action.expectedValue {
						t.Errorf("Get() = %v, want %v", got, action.expectedValue)
					}
				}
			}
		})
	}
}

func FuzzCache(f *testing.F) {
	// Define some seed values for initial scenarios
	for _, seed := range [][]byte{
		[]byte("7\x010\x0000000020"),
		{0, 0, 0, 0}, // Set key 0 to 0
		{1, 0, 0, 1}, // Set key 0 to 1
		{2, 0},       // Get key 0
	} {
		f.Add(seed)
	}

	f.Fuzz(func(t *testing.T, in []byte) {
		if len(in) < 1 {
			t.Skip() // Skip the test if the input is less than 1 byte
		}

		cache := NewMemoCache[HInt, int](10) // Initialize a cache with the initial size

		expectedValues := make(map[HInt]int) // Map to store expected key-value pairs
		accessOrder := make([]HInt, 0)       // Slice to store the order of keys accessed

		for i := 0; i < len(in); {
			opCode := in[i] % 4 // Determine the operation: Set, Get, or Reset (added case for Reset)
			i++

			switch opCode {
			case 0, 1: // Set operation
				if i+3 > len(in) {
					t.Skip() // Not enough input to continue, so skip
				}

				key := HInt(binary.BigEndian.Uint16(in[i : i+2]))
				value := int(in[i+2])
				i += 3

				// If the key is already in accessOrder, we remove it and append it again later
				for index, accessedKey := range accessOrder {
					if accessedKey == key {
						accessOrder = slices.Delete(accessOrder, index, index+1)
						break
					}
				}

				cache.Set(key, value) // Set the value in the cache
				expectedValues[key] = value
				accessOrder = append(accessOrder, key) // Add the key to the access order slice

				// If we exceeded the cache size, we need to evict the least recently used item
				if len(accessOrder) > cache.Capacity() {
					evictedKey := accessOrder[0]
					accessOrder = accessOrder[1:]
					delete(expectedValues, evictedKey) // Remove the evicted key from expected values
				}

			case 2: // Get operation
				if i >= len(in) {
					t.Skip() // Not enough input to continue, so skip
				}

				key := HInt(in[i])
				i++

				expectedValue, ok := expectedValues[key]
				if !ok {
					// If the key is not found, it means it was either evicted or never added
					expectedValue = 0 // The zero value, depends on your cache implementation
				} else {
					// If the key was accessed, move it to the end of the accessOrder to represent recent use
					for index, accessedKey := range accessOrder {
						if accessedKey == key {
							accessOrder = slices.Delete(accessOrder, index, index+1)
							accessOrder = append(accessOrder, key)
							break
						}
					}
				}

				if got, _ := cache.Get(key); got != expectedValue {
					fmt.Fprintf(os.Stderr, "cache: capacity: %d, hashable: %v, cache: %v\n", cache.capacity, cache.hashableItems, cache.cache)
					t.Fatalf("Get(%v) = %v, want %v", key, got, expectedValue) // The values do not match
				}
			case 3: // Reset operation
				if i >= len(in) {
					t.Skip() // Not enough input to continue, so skip
				}

				newCacheSize := int(in[i]) // Read the new cache size from the input
				i++

				if newCacheSize == 0 {
					t.Skip() // If the size is zero, we skip this test
				}

				// Create a new cache with the specified size
				cache = NewMemoCache[HInt, int](newCacheSize)

				// clear and reinitialize the expected values
				expectedValues = make(map[HInt]int)
				accessOrder = make([]HInt, 0)
			}
		}
	})
}


================================================
FILE: internal/runeutil/runeutil.go
================================================
// Package runeutil provides utility functions for tidying up incoming runes
// from Key messages.
package runeutil

import (
	"unicode"
	"unicode/utf8"
)

// Sanitizer is a helper for bubble widgets that want to process
// Runes from input key messages.
type Sanitizer interface {
	// Sanitize removes control characters from runes in a KeyRunes
	// message, and optionally replaces newline/carriage return/tabs by a
	// specified character.
	//
	// The rune array is modified in-place if possible. In that case, the
	// returned slice is the original slice shortened after the control
	// characters have been removed/translated.
	Sanitize(runes []rune) []rune
}

// NewSanitizer constructs a rune sanitizer.
func NewSanitizer(opts ...Option) Sanitizer {
	s := sanitizer{
		replaceNewLine: []rune("\n"),
		replaceTab:     []rune("    "),
	}
	for _, o := range opts {
		s = o(s)
	}
	return &s
}

// Option is the type of option that can be passed to Sanitize().
type Option func(sanitizer) sanitizer

// ReplaceTabs replaces tabs by the specified string.
func ReplaceTabs(tabRepl string) Option {
	return func(s sanitizer) sanitizer {
		s.replaceTab = []rune(tabRepl)
		return s
	}
}

// ReplaceNewlines replaces newline characters by the specified string.
func ReplaceNewlines(nlRepl string) Option {
	return func(s sanitizer) sanitizer {
		s.replaceNewLine = []rune(nlRepl)
		return s
	}
}

func (s *sanitizer) Sanitize(runes []rune) []rune {
	// dstrunes are where we are storing the result.
	dstrunes := runes[:0:len(runes)]
	// copied indicates whether dstrunes is an alias of runes
	// or a copy. We need a copy when dst moves past src.
	// We use this as an optimization to avoid allocating
	// a new rune slice in the common case where the output
	// is smaller or equal to the input.
	copied := false

	for src := range runes {
		r := runes[src]
		switch {
		case r == utf8.RuneError:
			// skip

		case r == '\r' || r == '\n':
			if len(dstrunes)+len(s.replaceNewLine) > src && !copied {
				dst := len(dstrunes)
				dstrunes = make([]rune, dst, len(runes)+len(s.replaceNewLine))
				copy(dstrunes, runes[:dst])
				copied = true
			}
			dstrunes = append(dstrunes, s.replaceNewLine...)

		case r == '\t':
			if len(dstrunes)+len(s.replaceTab) > src && !copied {
				dst := len(dstrunes)
				dstrunes = make([]rune, dst, len(runes)+len(s.replaceTab))
				copy(dstrunes, runes[:dst])
				copied = true
			}
			dstrunes = append(dstrunes, s.replaceTab...)

		case unicode.IsControl(r):
			// Other control characters: skip.

		default:
			// Keep the character.
			dstrunes = append(dstrunes, runes[src])
		}
	}
	return dstrunes
}

type sanitizer struct {
	replaceNewLine []rune
	replaceTab     []rune
}


================================================
FILE: internal/runeutil/runeutil_test.go
================================================
package runeutil

import (
	"testing"
	"unicode/utf8"
)

func TestSanitize(t *testing.T) {
	td := []struct {
		input, output string
	}{
		{"", ""},
		{"x", "x"},
		{"\n", "XX"},
		{"\na\n", "XXaXX"},
		{"\n\n", "XXXX"},
		{"\t", ""},
		{"hello", "hello"},
		{"hel\nlo", "helXXlo"},
		{"hel\rlo", "helXXlo"},
		{"hel\tlo", "hello"},
		{"he\n\nl\tlo", "heXXXXllo"},
		{"he\tl\n\nlo", "helXXXXlo"},
		{"hel\x1blo", "hello"},
		{"hello\xc2", "hello"}, // invalid utf8
	}

	for _, tc := range td {
		runes := make([]rune, 0, len(tc.input))
		b := []byte(tc.input)
		for i, w := 0, 0; i < len(b); i += w {
			var r rune
			r, w = utf8.DecodeRune(b[i:])
			runes = append(runes, r)
		}
		t.Logf("input runes: %+v", runes)
		s := NewSanitizer(ReplaceNewlines("XX"), ReplaceTabs(""))
		result := s.Sanitize(runes)
		rs := string(result)
		if tc.output != rs {
			t.Errorf("%q: expected %q, got %q (%+v)", tc.input, tc.output, rs, result)
		}
	}
}


================================================
FILE: key/key.go
================================================
// Package key provides some types and functions for generating user-definable
// keymappings useful in Bubble Tea components. There are a few different ways
// you can define a keymapping with this package. Here's one example:
//
//	type KeyMap struct {
//	    Up key.Binding
//	    Down key.Binding
//	}
//
//	var DefaultKeyMap = KeyMap{
//	    Up: key.NewBinding(
//	        key.WithKeys("k", "up"),        // actual keybindings
//	        key.WithHelp("↑/k", "move up"), // corresponding help text
//	    ),
//	    Down: key.NewBinding(
//	        key.WithKeys("j", "down"),
//	        key.WithHelp("↓/j", "move down"),
//	    ),
//	}
//
//	func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
//	    switch msg := msg.(type) {
//	    case tea.KeyPressMsg:
//	        switch {
//	        case key.Matches(msg, DefaultKeyMap.Up):
//	            // The user pressed up
//	        case key.Matches(msg, DefaultKeyMap.Down):
//	            // The user pressed down
//	        }
//	    }
//
//	    // ...
//	}
//
// The help information, which is not used in the example above, can be used
// to render help text for keystrokes in your views.
package key

import "fmt"

// Binding describes a set of keybindings and, optionally, their associated
// help text.
type Binding struct {
	keys     []string
	help     Help
	disabled bool
}

// BindingOpt is an initialization option for a keybinding. It's used as an
// argument to NewBinding.
type BindingOpt func(*Binding)

// NewBinding returns a new keybinding from a set of BindingOpt options.
func NewBinding(opts ...BindingOpt) Binding {
	b := &Binding{}
	for _, opt := range opts {
		opt(b)
	}
	return *b
}

// WithKeys initializes a keybinding with the given keystrokes.
func WithKeys(keys ...string) BindingOpt {
	return func(b *Binding) {
		b.keys = keys
	}
}

// WithHelp initializes a keybinding with the given help text.
func WithHelp(key, desc string) BindingOpt {
	return func(b *Binding) {
		b.help = Help{Key: key, Desc: desc}
	}
}

// WithDisabled initializes a disabled keybinding.
func WithDisabled() BindingOpt {
	return func(b *Binding) {
		b.disabled = true
	}
}

// SetKeys sets the keys for the keybinding.
func (b *Binding) SetKeys(keys ...string) {
	b.keys = keys
}

// Keys returns the keys for the keybinding.
func (b Binding) Keys() []string {
	return b.keys
}

// SetHelp sets the help text for the keybinding.
func (b *Binding) SetHelp(key, desc string) {
	b.help = Help{Key: key, Desc: desc}
}

// Help returns the Help information for the keybinding.
func (b Binding) Help() Help {
	return b.help
}

// Enabled returns whether or not the keybinding is enabled. Disabled
// keybindings won't be activated and won't show up in help. Keybindings are
// enabled by default.
func (b Binding) Enabled() bool {
	return !b.disabled && b.keys != nil
}

// SetEnabled enables or disables the keybinding.
func (b *Binding) SetEnabled(v bool) {
	b.disabled = !v
}

// Unbind removes the keys and help from this binding, effectively nullifying
// it. This is a step beyond disabling it, since applications can enable
// or disable key bindings based on application state.
func (b *Binding) Unbind() {
	b.keys = nil
	b.help = Help{}
}

// Help is help information for a given keybinding.
type Help struct {
	Key  string
	Desc string
}

// Matches checks if the given key matches the given bindings.
func Matches[Key fmt.Stringer](k Key, b ...Binding) bool {
	keys := k.String()
	for _, binding := range b {
		for _, v := range binding.keys {
			if keys == v && binding.Enabled() {
				return true
			}
		}
	}
	return false
}


================================================
FILE: key/key_test.go
================================================
package key

import (
	"testing"
)

func TestBinding_Enabled(t *testing.T) {
	binding := NewBinding(
		WithKeys("k", "up"),
		WithHelp("↑/k", "move up"),
	)
	if !binding.Enabled() {
		t.Errorf("expected key to be Enabled")
	}

	binding.SetEnabled(false)
	if binding.Enabled() {
		t.Errorf("expected key not to be Enabled")
	}

	binding.SetEnabled(true)
	binding.Unbind()
	if binding.Enabled() {
		t.Errorf("expected key not to be Enabled")
	}
}


================================================
FILE: list/README.md
================================================
# Frequently Asked Questions

These are some of the most commonly asked questions regarding the `list` bubble.

## Adding Custom Items

There are a few things you need to do to create custom items. First off, they
need to implement the `list.Item` and `list.DefaultItem` interfaces.

```go
// Item is an item that appears in the list.
type Item interface {
	// FilterValue is the value we use when filtering against this item when
	// we're filtering the list.
	FilterValue() string
}
```

```go
// DefaultItem describes an item designed to work with DefaultDelegate.
type DefaultItem interface {
	Item
	Title() string
	Description() string
}
```

You can see a working example in our [Kancli][kancli] project built
explicitly for a tutorial on lists and composite views in Bubble Tea.

[VIDEO](https://youtu.be/ZA93qgdLUzM)

## Customizing Styles

Rendering (and behavior) for list items is done via the
[`ItemDelegate`][itemDelegate]
interface. It can be a little confusing at first, but it allows the list to be
very flexible and powerful.

If you just want to alter the default style you could do something like:

```go
import "github.com/charmbracelet/bubbles/v2/list"

// Create a new default delegate
d := list.NewDefaultDelegate()

// Change colors
c := lipgloss.Color("#6f03fc")
d.Styles.SelectedTitle = d.Styles.SelectedTitle.Foreground(c).BorderLeftForeground(c)
d.Styles.SelectedDesc = d.Styles.SelectedTitle.Copy() // reuse the title style here

// Initailize the list model with our delegate
width, height := 80, 40
l := list.New(listItems, d, width, height)

// You can also change the delegate on the fly
l.SetDelegate(d)
```

This code would replace [this line][replacedLine] in the [`list-default`
example][listDefault].

For full control over the way list items are rendered you can also define your
own `ItemDelegate` too ([example][customDelegate]).

[kancli]: https://github.com/charmbracelet/kancli/blob/main/main.go#L45
[itemDelegate]: https://pkg.go.dev/github.com/charmbracelet/bubbles/list#ItemDelegate
[replacedLine]: https://github.com/charmbracelet/bubbletea/blob/main/examples/list-default/main.go#L77
[listDefault]: https://github.com/charmbracelet/bubbletea/tree/main/examples/list-default
[customDelegate]: https://github.com/charmbracelet/bubbletea/blob/main/examples/list-simple/main.go#L29-L50


================================================
FILE: list/defaultitem.go
================================================
package list

import (
	"fmt"
	"io"
	"strings"

	"charm.land/bubbles/v2/key"
	tea "charm.land/bubbletea/v2"
	"charm.land/lipgloss/v2"
	"github.com/charmbracelet/x/ansi"
)

// DefaultItemStyles defines styling for a default list item.
// See DefaultItemView for when these come into play.
type DefaultItemStyles struct {
	// The Normal state.
	NormalTitle lipgloss.Style
	NormalDesc  lipgloss.Style

	// The selected item state.
	SelectedTitle lipgloss.Style
	SelectedDesc  lipgloss.Style

	// The dimmed state, for when the filter input is initially activated.
	DimmedTitle lipgloss.Style
	DimmedDesc  lipgloss.Style

	// Characters matching the current filter, if any.
	FilterMatch lipgloss.Style
}

// NewDefaultItemStyles returns style definitions for a default item. See
// DefaultItemView for when these come into play.
func NewDefaultItemStyles(isDark bool) (s DefaultItemStyles) {
	lightDark := lipgloss.LightDark(isDark)

	s.NormalTitle = lipgloss.NewStyle().
		Foreground(lightDark(lipgloss.Color("#1a1a1a"), lipgloss.Color("#dddddd"))).
		Padding(0, 0, 0, 2) //nolint:mnd

	s.NormalDesc = s.NormalTitle.
		Foreground(lightDark(lipgloss.Color("#A49FA5"), lipgloss.Color("#777777")))

	s.SelectedTitle = lipgloss.NewStyle().
		Border(lipgloss.NormalBorder(), false, false, false, true).
		BorderForeground(lightDark(lipgloss.Color("#F793FF"), lipgloss.Color("#AD58B4"))).
		Foreground(lightDark(lipgloss.Color("#EE6FF8"), lipgloss.Color("#EE6FF8"))).
		Padding(0, 0, 0, 1)

	s.SelectedDesc = s.SelectedTitle.
		Foreground(lightDark(lipgloss.Color("#F793FF"), lipgloss.Color("#AD58B4")))

	s.DimmedTitle = lipgloss.NewStyle().
		Foreground(lightDark(lipgloss.Color("#A49FA5"), lipgloss.Color("#777777"))).
		Padding(0, 0, 0, 2) //nolint:mnd

	s.DimmedDesc = s.DimmedTitle.
		Foreground(lightDark(lipgloss.Color("#C2B8C2"), lipgloss.Color("#4D4D4D")))

	s.FilterMatch = lipgloss.NewStyle().Underline(true)

	return s
}

// DefaultItem describes an item designed to work with DefaultDelegate.
type DefaultItem interface {
	Item
	Title() string
	Description() string
}

// DefaultDelegate is a standard delegate designed to work in lists. It's
// styled by DefaultItemStyles, which can be customized as you like.
//
// The description line can be hidden by setting Description to false, which
// renders the list as single-line-items. The spacing between items can be set
// with the SetSpacing method.
//
// Setting UpdateFunc is optional. If it's set it will be called when the
// ItemDelegate called, which is called when the list's Update function is
// invoked.
//
// Settings ShortHelpFunc and FullHelpFunc is optional. They can be set to
// include items in the list's default short and full help menus.
type DefaultDelegate struct {
	ShowDescription bool
	Styles          DefaultItemStyles
	UpdateFunc      func(tea.Msg, *Model) tea.Cmd
	ShortHelpFunc   func() []key.Binding
	FullHelpFunc    func() [][]key.Binding
	height          int
	spacing         int
}

// NewDefaultDelegate creates a new delegate with default styles.
func NewDefaultDelegate() DefaultDelegate {
	const defaultHeight = 2
	const defaultSpacing = 1
	return DefaultDelegate{
		ShowDescription: true,
		// XXX: Let the user choose between light and dark colors. We've
		// temporarily hardcoded the dark colors here.
		Styles:  NewDefaultItemStyles(true),
		height:  defaultHeight,
		spacing: defaultSpacing,
	}
}

// SetHeight sets delegate's preferred height.
func (d *DefaultDelegate) SetHeight(i int) {
	d.height = i
}

// Height returns the delegate's preferred height.
// This has effect only if ShowDescription is true,
// otherwise height is always 1.
func (d DefaultDelegate) Height() int {
	if d.ShowDescription {
		return d.height
	}
	return 1
}

// SetSpacing sets the delegate's spacing.
func (d *DefaultDelegate) SetSpacing(i int) {
	d.spacing = i
}

// Spacing returns the delegate's spacing.
func (d DefaultDelegate) Spacing() int {
	return d.spacing
}

// Update checks whether the delegate's UpdateFunc is set and calls it.
func (d DefaultDelegate) Update(msg tea.Msg, m *Model) tea.Cmd {
	if d.UpdateFunc == nil {
		return nil
	}
	return d.UpdateFunc(msg, m)
}

// Render prints an item.
func (d DefaultDelegate) Render(w io.Writer, m Model, index int, item Item) {
	var (
		title, desc  string
		matchedRunes []int
		s            = &d.Styles
	)

	if i, ok := item.(DefaultItem); ok {
		title = i.Title()
		desc = i.Description()
	} else {
		return
	}

	if m.width <= 0 {
		// short-circuit
		return
	}

	// Prevent text from exceeding list width
	textwidth := m.width - s.NormalTitle.GetPaddingLeft() - s.NormalTitle.GetPaddingRight()
	title = ansi.Truncate(title, textwidth, ellipsis)
	if d.ShowDescription {
		var lines []string
		for i, line := range strings.Split(desc, "\n") {
			if i >= d.height-1 {
				break
			}
			lines = append(lines, ansi.Truncate(line, textwidth, ellipsis))
		}
		desc = strings.Join(lines, "\n")
	}

	// Conditions
	var (
		isSelected  = index == m.Index()
		emptyFilter = m.FilterState() == Filtering && m.FilterValue() == ""
		isFiltered  = m.FilterState() == Filtering || m.FilterState() == FilterApplied
	)

	if isFiltered && index < len(m.filteredItems) {
		// Get indices of matched characters
		matchedRunes = m.MatchesForItem(index)
	}

	if emptyFilter {
		title = s.DimmedTitle.Render(title)
		desc = s.DimmedDesc.Render(desc)
	} else if isSelected && m.FilterState() != Filtering {
		if isFiltered {
			// Highlight matches
			unmatched := s.SelectedTitle.Inline(true)
			matched := unmatched.Inherit(s.FilterMatch)
			title = lipgloss.StyleRunes(title, matchedRunes, matched, unmatched)
		}
		title = s.SelectedTitle.Render(title)
		desc = s.SelectedDesc.Render(desc)
	} else {
		if isFiltered {
			// Highlight matches
			unmatched := s.NormalTitle.Inline(true)
			matched := unmatched.Inherit(s.FilterMatch)
			title = lipgloss.StyleRunes(title, matchedRunes, matched, unmatched)
		}
		title = s.NormalTitle.Render(title)
		desc = s.NormalDesc.Render(desc)
	}

	if d.ShowDescription {
		fmt.Fprintf(w, "%s\n%s", title, desc) //nolint: errcheck
		return
	}
	fmt.Fprintf(w, "%s", title) //nolint: errcheck
}

// ShortHelp returns the delegate's short help.
func (d DefaultDelegate) ShortHelp() []key.Binding {
	if d.ShortHelpFunc != nil {
		return d.ShortHelpFunc()
	}
	return nil
}

// FullHelp returns the delegate's full help.
func (d DefaultDelegate) FullHelp() [][]key.Binding {
	if d.FullHelpFunc != nil {
		return d.FullHelpFunc()
	}
	return nil
}


================================================
FILE: list/keys.go
================================================
package list

import "charm.land/bubbles/v2/key"

// KeyMap defines keybindings. It satisfies to the help.KeyMap interface, which
// is used to render the menu.
type KeyMap struct {
	// Keybindings used when browsing the list.
	CursorUp    key.Binding
	CursorDown  key.Binding
	NextPage    key.Binding
	PrevPage    key.Binding
	GoToStart   key.Binding
	GoToEnd     key.Binding
	Filter      key.Binding
	ClearFilter key.Binding

	// Keybindings used when setting a filter.
	CancelWhileFiltering key.Binding
	AcceptWhileFiltering key.Binding

	// Help toggle keybindings.
	ShowFullHelp  key.Binding
	CloseFullHelp key.Binding

	// The quit keybinding. This won't be caught when filtering.
	Quit key.Binding

	// The quit-no-matter-what keybinding. This will be caught when filtering.
	ForceQuit key.Binding
}

// DefaultKeyMap returns a default set of keybindings.
func DefaultKeyMap() KeyMap {
	return KeyMap{
		// Browsing.
		CursorUp: key.NewBinding(
			key.WithKeys("up", "k"),
			key.WithHelp("↑/k", "up"),
		),
		CursorDown: key.NewBinding(
			key.WithKeys("down", "j"),
			key.WithHelp("↓/j", "down"),
		),
		PrevPage: key.NewBinding(
			key.WithKeys("left", "h", "pgup", "b", "u"),
			key.WithHelp("←/h/pgup", "prev page"),
		),
		NextPage: key.NewBinding(
			key.WithKeys("right", "l", "pgdown", "f", "d"),
			key.WithHelp("→/l/pgdn", "next page"),
		),
		GoToStart: key.NewBinding(
			key.WithKeys("home", "g"),
			key.WithHelp("g/home", "go to start"),
		),
		GoToEnd: key.NewBinding(
			key.WithKeys("end", "G"),
			key.WithHelp("G/end", "go to end"),
		),
		Filter: key.NewBinding(
			key.WithKeys("/"),
			key.WithHelp("/", "filter"),
		),
		ClearFilter: key.NewBinding(
			key.WithKeys("esc"),
			key.WithHelp("esc", "clear filter"),
		),

		// Filtering.
		CancelWhileFiltering: key.NewBinding(
			key.WithKeys("esc"),
			key.WithHelp("esc", "cancel"),
		),
		AcceptWhileFiltering: key.NewBinding(
			key.WithKeys("enter", "tab", "shift+tab", "ctrl+k", "up", "ctrl+j", "down"),
			key.WithHelp("enter", "apply filter"),
		),

		// Toggle help.
		ShowFullHelp: key.NewBinding(
			key.WithKeys("?"),
			key.WithHelp("?", "more"),
		),
		CloseFullHelp: key.NewBinding(
			key.WithKeys("?"),
			key.WithHelp("?", "close help"),
		),

		// Quitting.
		Quit: key.NewBinding(
			key.WithKeys("q", "esc"),
			key.WithHelp("q", "quit"),
		),
		ForceQuit: key.NewBinding(key.WithKeys("ctrl+c")),
	}
}


================================================
FILE: list/list.go
================================================
// Package list provides a feature-rich Bubble Tea component for browsing
// a general purpose list of items. It features optional filtering, pagination,
// help, status messages, and a spinner to indicate activity.
package list

import (
	"cmp"
	"fmt"
	"io"
	"sort"
	"strings"
	"time"

	tea "charm.land/bubbletea/v2"
	"charm.land/lipgloss/v2"
	"github.com/charmbracelet/x/ansi"
	"github.com/sahilm/fuzzy"

	"charm.land/bubbles/v2/help"
	"charm.land/bubbles/v2/key"
	"charm.land/bubbles/v2/paginator"
	"charm.land/bubbles/v2/spinner"
	"charm.land/bubbles/v2/textinput"
)

func clamp[T cmp.Ordered](v, low, high T) T {
	if low > high {
		low, high = high, low
	}
	return min(high, max(low, v))
}

// Item is an item that appears in the list.
type Item interface {
	// FilterValue is the value we use when filtering against this item when
	// we're filtering the list.
	FilterValue() string
}

// ItemDelegate encapsulates the general functionality for all list items. The
// benefit to separating this logic from the item itself is that you can change
// the functionality of items without changing the actual items themselves.
//
// Note that if the delegate also implements help.KeyMap delegate-related
// help items will be added to the help view.
type ItemDelegate interface {
	// Render renders the item's view.
	Render(w io.Writer, m Model, index int, item Item)

	// Height is the height of the list item.
	Height() int

	// Spacing is the size of the horizontal gap between list items in cells.
	Spacing() int

	// Update is the update loop for items. All messages in the list's update
	// loop will pass through here except when the user is setting a filter.
	// Use this method to perform item-level updates appropriate to this
	// delegate.
	Update(msg tea.Msg, m *Model) tea.Cmd
}

type filteredItem struct {
	index   int   // index in the unfiltered list
	item    Item  // item matched
	matches []int // rune indices of matched items
}

type filteredItems []filteredItem

func (f filteredItems) items() []Item {
	agg := make([]Item, len(f))
	for i, v := range f {
		agg[i] = v.item
	}
	return agg
}

// FilterMatchesMsg contains data about items matched during filtering. The
// message should be routed to Update for processing.
type FilterMatchesMsg []filteredItem

// FilterFunc takes a term and a list of strings to search through
// (defined by Item#FilterValue).
// It should return a sorted list of ranks.
type FilterFunc func(string, []string) []Rank

// Rank defines a rank for a given item.
type Rank struct {
	// The index of the item in the original input.
	Index int
	// Indices of the actual word that were matched against the filter term.
	MatchedIndexes []int
}

// DefaultFilter uses the sahilm/fuzzy to filter through the list.
// This is set by default.
func DefaultFilter(term string, targets []string) []Rank {
	ranks := fuzzy.Find(term, targets)
	sort.Stable(ranks)
	result := make([]Rank, len(ranks))
	for i, r := range ranks {
		result[i] = Rank{
			Index:          r.Index,
			MatchedIndexes: r.MatchedIndexes,
		}
	}
	return result
}

// UnsortedFilter uses the sahilm/fuzzy to filter through the list. It does not
// sort the results.
func UnsortedFilter(term string, targets []string) []Rank {
	ranks := fuzzy.FindNoSort(term, targets)
	result := make([]Rank, len(ranks))
	for i, r := range ranks {
		result[i] = Rank{
			Index:          r.Index,
			MatchedIndexes: r.MatchedIndexes,
		}
	}
	return result
}

type statusMessageTimeoutMsg struct{}

// FilterState describes the current filtering state on the model.
type FilterState int

// Possible filter states.
const (
	Unfiltered    FilterState = iota // no filter set
	Filtering                        // user is actively setting a filter
	FilterApplied                    // a filter is applied and user is not editing filter
)

// String returns a human-readable string of the current filter state.
func (f FilterState) String() string {
	return [...]string{
		"unfiltered",
		"filtering",
		"filter applied",
	}[f]
}

// Model contains the state of this component.
type Model struct {
	showTitle        bool
	showFilter       bool
	showStatusBar    bool
	showPagination   bool
	showHelp         bool
	filteringEnabled bool

	itemNameSingular string
	itemNamePlural   string

	Title             string
	Styles            Styles
	InfiniteScrolling bool

	// Key mappings for navigating the list.
	KeyMap KeyMap

	// Filter is used to filter the list.
	Filter FilterFunc

	disableQuitKeybindings bool

	// Additional key mappings for the short and full help views. This allows
	// you to add additional key mappings to the help menu without
	// re-implementing the help component. Of course, you can also disable the
	// list's help component and implement a new one if you need more
	// flexibility.
	AdditionalShortHelpKeys func() []key.Binding
	AdditionalFullHelpKeys  func() []key.Binding

	spinner     spinner.Model
	showSpinner bool
	width       int
	height      int
	Paginator   paginator.Model
	cursor      int
	Help        help.Model
	FilterInput textinput.Model
	filterState FilterState

	// How long status messages should stay visible. By default this is
	// 1 second.
	StatusMessageLifetime time.Duration

	statusMessage      string
	statusMessageTimer *time.Timer

	// The master set of items we're working with.
	items []Item

	// Filtered items we're currently displaying. Filtering, toggles and so on
	// will alter this slice so we can show what is relevant. For that reason,
	// this field should be considered ephemeral.
	filteredItems filteredItems

	delegate ItemDelegate
}

// New returns a new model with sensible defaults.
func New(items []Item, delegate ItemDelegate, width, height int) Model {
	// XXX: Let the user choose between light and dark colors. We've
	// temporarily hardcoded the dark colors here.
	styles := DefaultStyles(true)

	sp := spinner.New()
	sp.Spinner = spinner.Line
	sp.Style = styles.Spinner

	filterInput := textinput.New()
	filterInput.Prompt = "Filter: "
	filterInput.CharLimit = 64
	filterInput.Focus()

	p := paginator.New()
	p.Type = paginator.Dots
	p.ActiveDot = styles.ActivePaginationDot.String()
	p.InactiveDot = styles.InactivePaginationDot.String()

	m := Model{
		showTitle:             true,
		showFilter:            true,
		showStatusBar:         true,
		showPagination:        true,
		showHelp:              true,
		itemNameSingular:      "item",
		itemNamePlural:        "items",
		filteringEnabled:      true,
		KeyMap:                DefaultKeyMap(),
		Filter:                DefaultFilter,
		Styles:                styles,
		Title:                 "List",
		FilterInput:           filterInput,
		StatusMessageLifetime: time.Second,

		width:     width,
		height:    height,
		delegate:  delegate,
		items:     items,
		Paginator: p,
		spinner:   sp,
		Help:      help.New(),
	}

	m.updatePagination()
	m.updateKeybindings()
	return m
}

// SetFilteringEnabled enables or disables filtering. Note that this is different
// from ShowFilter, which merely hides or shows the input view.
func (m *Model) SetFilteringEnabled(v bool) {
	m.filteringEnabled = v
	if !v {
		m.resetFiltering()
	}
	m.updateKeybindings()
}

// FilteringEnabled returns whether or not filtering is enabled.
func (m Model) FilteringEnabled() bool {
	return m.filteringEnabled
}

// SetShowTitle shows or hides the title bar.
func (m *Model) SetShowTitle(v bool) {
	m.showTitle = v
	m.updatePagination()
}

// SetFilterText explicitly sets the filter text without relying on user input.
// It also sets the filterState to a sane default of FilterApplied, but this
// can be changed with SetFilterState.
func (m *Model) SetFilterText(filter string) {
	m.filterState = Filtering
	m.FilterInput.SetValue(filter)
	cmd := filterItems(*m)
	msg := cmd()
	fmm, _ := msg.(FilterMatchesMsg)
	m.filteredItems = filteredItems(fmm)
	m.filterState = FilterApplied
	m.GoToStart()
	m.FilterInput.CursorEnd()
	m.updatePagination()
	m.updateKeybindings()
}

// SetFilterState allows setting the filtering state manually.
func (m *Model) SetFilterState(state FilterState) {
	m.GoToStart()
	m.filterState = state
	m.FilterInput.CursorEnd()
	m.FilterInput.Focus()
	m.updateKeybindings()
}

// ShowTitle returns whether or not the title bar is set to be rendered.
func (m Model) ShowTitle() bool {
	return m.showTitle
}

// SetShowFilter shows or hides the filter bar. Note that this does not disable
// filtering, it simply hides the built-in filter view. This allows you to
// use the FilterInput to render the filtering UI differently without having to
// re-implement filtering from scratch.
//
// To disable filtering entirely use EnableFiltering.
func (m *Model) SetShowFilter(v bool) {
	m.showFilter = v
	m.updatePagination()
}

// ShowFilter returns whether or not the filter is set to be rendered. Note
// that this is separate from FilteringEnabled, so filtering can be hidden yet
// still invoked. This allows you to render filtering differently without
// having to re-implement it from scratch.
func (m Model) ShowFilter() bool {
	return m.showFilter
}

// SetShowStatusBar shows or hides the view that displays metadata about the
// list, such as item counts.
func (m *Model) SetShowStatusBar(v bool) {
	m.showStatusBar = v
	m.updatePagination()
}

// ShowStatusBar returns whether or not the status bar is set to be rendered.
func (m Model) ShowStatusBar() bool {
	return m.showStatusBar
}

// SetStatusBarItemName defines a replacement for the item's identifier.
// Defaults to item/items.
func (m *Model) SetStatusBarItemName(singular, plural string) {
	m.itemNameSingular = singular
	m.itemNamePlural = plural
}

// StatusBarItemName returns singular and plural status bar item names.
func (m Model) StatusBarItemName() (string, string) {
	return m.itemNameSingular, m.itemNamePlural
}

// SetShowPagination hides or shows the paginator. Note that pagination will
// still be active, it simply won't be displayed.
func (m *Model) SetShowPagination(v bool) {
	m.showPagination = v
	m.updatePagination()
}

// ShowPagination returns whether the pagination is visible.
func (m *Model) ShowPagination() bool {
	return m.showPagination
}

// SetShowHelp shows or hides the help view.
func (m *Model) SetShowHelp(v bool) {
	m.showHelp = v
	m.updatePagination()
}

// ShowHelp returns whether or not the help is set to be rendered.
func (m Model) ShowHelp() bool {
	return m.showHelp
}

// Items returns the items in the list.
func (m Model) Items() []Item {
	return m.items
}

// SetItems sets the items available in the list. This returns a command.
func (m *Model) SetItems(i []Item) tea.Cmd {
	var cmd tea.Cmd
	m.items = i

	if m.filterState != Unfiltered {
		m.filteredItems = nil
		cmd = filterItems(*m)
	}

	m.updatePagination()
	m.updateKeybindings()
	return cmd
}

// Select selects the given index of the list and goes to its respective page.
func (m *Model) Select(index int) {
	m.Paginator.Page = index / m.Paginator.PerPage
	m.cursor = index % m.Paginator.PerPage
}

// ResetSelected resets the selected item to the first item in the first page of the list.
func (m *Model) ResetSelected() {
	m.Select(0)
}

// ResetFilter resets the current filtering state.
func (m *Model) ResetFilter() {
	m.resetFiltering()
}

// SetItem replaces an item at the given index. This returns a command.
func (m *Model) SetItem(index int, item Item) tea.Cmd {
	var cmd tea.Cmd
	m.items[index] = item

	if m.filterState != Unfiltered {
		cmd = filterItems(*m)
	}

	m.updatePagination()
	return cmd
}

// InsertItem inserts an item at the given index. If the index is out of the upper bound,
// the item will be appended. This returns a command.
func (m *Model) InsertItem(index int, item Item) tea.Cmd {
	var cmd tea.Cmd
	m.items = insertItemIntoSlice(m.items, item, index)

	if m.filterState != Unfiltered {
		cmd = filterItems(*m)
	}

	m.updatePagination()
	m.updateKeybindings()
	return cmd
}

// RemoveItem removes an item at the given index. If the index is out of bounds
// this will be a no-op. O(n) complexity, which probably won't matter in the
// case of a TUI.
func (m *Model) RemoveItem(index int) {
	m.items = removeItemFromSlice(m.items, index)
	if m.filterState != Unfiltered {
		m.filteredItems = removeFilterMatchFromSlice(m.filteredItems, index)
		if len(m.filteredItems) == 0 {
			m.resetFiltering()
		}
	}
	m.updatePagination()
}

// SetDelegate sets the item delegate.
func (m *Model) SetDelegate(d ItemDelegate) {
	m.delegate = d
	m.updatePagination()
}

// VisibleItems returns the total items available to be shown.
func (m Model) VisibleItems() []Item {
	if m.filterState != Unfiltered {
		return m.filteredItems.items()
	}
	return m.items
}

// SelectedItem returns the current selected item in the list.
func (m Model) SelectedItem() Item {
	i := m.Index()

	items := m.VisibleItems()
	if i < 0 || len(items) == 0 || len(items) <= i {
		return nil
	}

	return items[i]
}

// MatchesForItem returns rune positions matched by the current filter, if any.
// Use this to style runes matched by the active filter.
//
// See DefaultItemView for a usage example.
func (m Model) MatchesForItem(index int) []int {
	if m.filteredItems == nil || index >= len(m.filteredItems) {
		return nil
	}
	return m.filteredItems[index].matches
}

// Index returns the index of the currently selected item as it is stored in the
// filtered list of items.
// Using this value with SetItem() might be incorrect, consider using
// GlobalIndex() instead.
func (m Model) Index() int {
	return m.Paginator.Page*m.Paginator.PerPage + m.cursor
}

// GlobalIndex returns the index of the currently selected item as it is stored
// in the unfiltered list of items. This value can be used with SetItem().
func (m Model) GlobalIndex() int {
	index := m.Index()

	if m.filteredItems == nil || index >= len(m.filteredItems) {
		return index
	}

	return m.filteredItems[index].index
}

// Cursor returns the index of the cursor on the current page.
func (m Model) Cursor() int {
	return m.cursor
}

// CursorUp moves the cursor up. This can also move the state to the previous
// page.
func (m *Model) CursorUp() {
	m.cursor--

	// If we're at the start, stop
	if m.cursor < 0 && m.Paginator.OnFirstPage() {
		// if infinite scrolling is enabled, go to the last item
		if m.InfiniteScrolling {
			m.GoToEnd()
			return
		}
		m.cursor = 0
		return
	}

	// Move the cursor as normal
	if m.cursor >= 0 {
		return
	}

	// Go to the previous page
	m.Paginator.PrevPage()
	m.cursor = m.maxCursorIndex()
}

// CursorDown moves the cursor down. This can also advance the state to the
// next page.
func (m *Model) CursorDown() {
	maxCursorIndex := m.maxCursorIndex()

	m.cursor++

	// We're still within bounds of the current page, so no need to do anything.
	if m.cursor <= maxCursorIndex {
		return
	}

	// Go to the next page
	if !m.Paginator.OnLastPage() {
		m.Paginator.NextPage()
		m.cursor = 0
		return
	}

	m.cursor = max(0, maxCursorIndex)

	// if infinite scrolling is enabled, go to the first item.
	if m.InfiniteScrolling {
		m.GoToStart()
	}
}

// GoToStart moves to the first page, and first item on the first page.
func (m *Model) GoToStart() {
	m.Paginator.Page = 0
	m.cursor = 0
}

// GoToEnd moves to the last page, and last item on the last page.
func (m *Model) GoToEnd() {
	m.Paginator.Page = max(0, m.Paginator.TotalPages-1)
	m.cursor = m.maxCursorIndex()
}

// PrevPage moves to the previous page, if available.
func (m *Model) PrevPage() {
	m.Paginator.PrevPage()
	m.cursor = clamp(m.cursor, 0, m.maxCursorIndex())
}

// NextPage moves to the next page, if available.
func (m *Model) NextPage() {
	m.Paginator.NextPage()
	m.cursor = clamp(m.cursor, 0, m.maxCursorIndex())
}

func (m *Model) maxCursorIndex() int {
	return max(0, m.Paginator.ItemsOnPage(len(m.VisibleItems()))-1)
}

// FilterState returns the current filter state.
func (m Model) FilterState() FilterState {
	return m.filterState
}

// FilterValue returns the current value of the filter.
func (m Model) FilterValue() string {
	return m.FilterInput.Value()
}

// SettingFilter returns whether or not the user is currently editing the
// filter value. It's purely a convenience method for the following:
//
//	m.FilterState() == Filtering
//
// It's included here because it's a common thing to check for when
// implementing this component.
func (m Model) SettingFilter() bool {
	return m.filterState == Filtering
}

// IsFiltered returns whether or not the list is currently filtered.
// It's purely a convenience method for the following:
//
//	m.FilterState() == FilterApplied
func (m Model) IsFiltered() bool {
	return m.filterState == FilterApplied
}

// Width returns the current width setting.
func (m Model) Width() int {
	return m.width
}

// Height returns the current height setting.
func (m Model) Height() int {
	return m.height
}

// SetSpinner allows to set the spinner style.
func (m *Model) SetSpinner(spinner spinner.Spinner) {
	m.spinner.Spinner = spinner
}

// ToggleSpinner toggles the spinner. Note that this also returns a command.
func (m *Model) ToggleSpinner() tea.Cmd {
	if !m.showSpinner {
		return m.StartSpinner()
	}
	m.StopSpinner()
	return nil
}

// StartSpinner starts the spinner. Note that this returns a command.
func (m *Model) StartSpinner() tea.Cmd {
	m.showSpinner = true
	return m.spinner.Tick
}

// StopSpinner stops the spinner.
func (m *Model) StopSpinner() {
	m.showSpinner = false
}

// DisableQuitKeybindings is a helper for disabling the keybindings used for quitting,
// in case you want to handle this elsewhere in your application.
func (m *Model) DisableQuitKeybindings() {
	m.disableQuitKeybindings = true
	m.KeyMap.Quit.SetEnabled(false)
	m.KeyMap.ForceQuit.SetEnabled(false)
}

// NewStatusMessage sets a new status message, which will show for a limited
// amount of time. Note that this also returns a command.
func (m *Model) NewStatusMessage(s string) tea.Cmd {
	m.statusMessage = s
	if m.statusMessageTimer != nil {
		m.statusMessageTimer.Stop()
	}

	m.statusMessageTimer = time.NewTimer(m.StatusMessageLifetime)

	// Wait for timeout
	return func() tea.Msg {
		<-m.statusMessageTimer.C
		return statusMessageTimeoutMsg{}
	}
}

// SetWidth sets the width of this component.
func (m *Model) SetWidth(v int) {
	m.SetSize(v, m.height)
}

// SetHeight sets the height of this component.
func (m *Model) SetHeight(v int) {
	m.SetSize(m.width, v)
}

// SetSize sets the width and height of this component.
func (m *Model) SetSize(width, height int) {
	promptWidth := lipgloss.Width(m.Styles.Title.Render(m.FilterInput.Prompt))

	m.width = width
	m.height = height
	m.Help.SetWidth(width)
	m.FilterInput.SetWidth(width - promptWidth - lipgloss.Width(m.spinnerView()))
	m.updatePagination()
	m.updateKeybindings()
}

func (m *Model) resetFiltering() {
	if m.filterState == Unfiltered {
		return
	}

	m.filterState = Unfiltered
	m.FilterInput.Reset()
	m.filteredItems = nil
	m.updatePagination()
	m.updateKeybindings()
}

func (m Model) itemsAsFilterItems() filteredItems {
	fi := make([]filteredItem, len(m.items))
	for i, item := range m.items {
		fi[i] = filteredItem{
			item: item,
		}
	}
	return fi
}

// Set keybindings according to the filter state.
func (m *Model) updateKeybindings() {
	switch m.filterState { //nolint:exhaustive
	case Filtering:
		m.KeyMap.CursorUp.SetEnabled(false)
		m.KeyMap.CursorDown.SetEnabled(false)
		m.KeyMap.NextPage.SetEnabled(false)
		m.KeyMap.PrevPage.SetEnabled(false)
		m.KeyMap.GoToStart.SetEnabled(false)
		m.KeyMap.GoToEnd.SetEnabled(false)
		m.KeyMap.Filter.SetEnabled(false)
		m.KeyMap.ClearFilter.SetEnabled(false)
		m.KeyMap.CancelWhileFiltering.SetEnabled(true)
		m.KeyMap.AcceptWhileFiltering.SetEnabled(m.FilterInput.Value() != "")
		m.KeyMap.Quit.SetEnabled(false)
		m.KeyMap.ShowFullHelp.SetEnabled(false)
		m.KeyMap.CloseFullHelp.SetEnabled(false)

	default:
		hasItems := len(m.items) != 0
		m.KeyMap.CursorUp.SetEnabled(hasItems)
		m.KeyMap.CursorDown.SetEnabled(hasItems)

		hasPages := m.Paginator.TotalPages > 1
		m.KeyMap.NextPage.SetEnabled(hasPages)
		m.KeyMap.PrevPage.SetEnabled(hasPages)

		m.KeyMap.GoToStart.SetEnabled(hasItems)
		m.KeyMap.GoToEnd.SetEnabled(hasItems)

		m.KeyMap.Filter.SetEnabled(m.filteringEnabled && hasItems)
		m.KeyMap.ClearFilter.SetEnabled(m.filterState == FilterApplied)
		m.KeyMap.CancelWhileFiltering.SetEnabled(false)
		m.KeyMap.AcceptWhileFiltering.SetEnabled(false)
		m.KeyMap.Quit.SetEnabled(!m.disableQuitKeybindings)

		if m.Help.ShowAll {
			m.KeyMap.ShowFullHelp.SetEnabled(true)
			m.KeyMap.CloseFullHelp.SetEnabled(true)
		} else {
			minHelp := countEnabledBindings(m.FullHelp()) > 1
			m.KeyMap.ShowFullHelp.SetEnabled(minHelp)
			m.KeyMap.CloseFullHelp.SetEnabled(minHelp)
		}
	}
}

// Update pagination according to the amount of items for the current state.
func (m *Model) updatePagination() {
	index := m.Index()
	availHeight := m.height

	if m.showTitle || (m.showFilter && m.filteringEnabled) {
		availHeight -= lipgloss.Height(m.titleView())
	}
	if m.showStatusBar {
		availHeight -= lipgloss.Height(m.statusView())
	}
	if m.showPagination {
		availHeight -= lipgloss.Height(m.paginationView())
	}
	if m.showHelp {
		availHeight -= lipgloss.Height(m.helpView())
	}

	m.Paginator.PerPage = max(1, availHeight/(m.delegate.Height()+m.delegate.Spacing()))

	if pages := len(m.VisibleItems()); pages < 1 {
		m.Paginator.SetTotalPages(1)
	} else {
		m.Paginator.SetTotalPages(pages)
	}

	// Restore index
	m.Paginator.Page = index / m.Paginator.PerPage
	m.cursor = index % m.Paginator.PerPage

	// Make sure the page stays in bounds
	if m.Paginator.Page >= m.Paginator.TotalPages-1 {
		m.Paginator.Page = max(0, m.Paginator.TotalPages-1)
	}
}

func (m *Model) hideStatusMessage() {
	m.statusMessage = ""
	if m.statusMessageTimer != nil {
		m.statusMessageTimer.Stop()
	}
}

// Update is the Bubble Tea update loop.
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
	var cmds []tea.Cmd

	switch msg := msg.(type) {
	case tea.KeyPressMsg:
		if key.Matches(msg, m.KeyMap.ForceQuit) {
			return m, tea.Quit
		}

	case FilterMatchesMsg:
		m.filteredItems = filteredItems(msg)
		return m, nil

	case spinner.TickMsg:
		newSpinnerModel, cmd := m.spinner.Update(msg)
		m.spinner = newSpinnerModel
		if m.showSpinner {
			cmds = append(cmds, cmd)
		}

	case statusMessageTimeoutMsg:
		m.hideStatusMessage()
	}

	if m.filterState == Filtering {
		cmds = append(cmds, m.handleFiltering(msg))
	} else {
		cmds = append(cmds, m.handleBrowsing(msg))
	}

	return m, tea.Batch(cmds...)
}

// Updates for when a user is browsing the list.
func (m *Model) handleBrowsing(msg tea.Msg) tea.Cmd {
	switch msg := msg.(type) {
	case tea.KeyPressMsg:
		switch {
		// Note: we match clear filter before quit because, by default, they're
		// both mapped to escape.
		case key.Matches(msg, m.KeyMap.ClearFilter):
			m.resetFiltering()

		case key.Matches(msg, m.KeyMap.Quit):
			return tea.Quit

		case key.Matches(msg, m.KeyMap.CursorUp):
			m.CursorUp()

		case key.Matches(msg, m.KeyMap.CursorDown):
			m.CursorDown()

		case key.Matches(msg, m.KeyMap.PrevPage):
			m.Paginator.PrevPage()

		case key.Matches(msg, m.KeyMap.NextPage):
			m.Paginator.NextPage()

		case key.Matches(msg, m.KeyMap.GoToStart):
			m.GoToStart()

		case key.Matches(msg, m.KeyMap.GoToEnd):
			m.GoToEnd()

		case key.Matches(msg, m.KeyMap.Filter):
			m.hideStatusMessage()
			if m.FilterInput.Value() == "" {
				// Populate filter with all items only if the filter is empty.
				m.filteredItems = m.itemsAsFilterItems()
			}
			m.GoToStart()
			m.filterState = Filtering
			m.FilterInput.CursorEnd()
			m.FilterInput.Focus()
			m.updateKeybindings()
			return textinput.Blink

		case key.Matches(msg, m.KeyMap.ShowFullHelp):
			fallthrough
		case key.Matches(msg, m.KeyMap.CloseFullHelp):
			m.Help.ShowAll = !m.Help.ShowAll
			m.updatePagination()
		}
	}

	cmd := m.delegate.Update(msg, m)
	m.cursor = clamp(m.cursor, 0, m.maxCursorIndex())

	return cmd
}

// Updates for when a user is in the filter editing interface.
func (m *Model) handleFiltering(msg tea.Msg) tea.Cmd {
	var cmds []tea.Cmd

	// Handle keys
	if msg, ok := msg.(tea.KeyPressMsg); ok {
		switch {
		case key.Matches(msg, m.KeyMap.CancelWhileFiltering):
			m.resetFiltering()
			m.KeyMap.Filter.SetEnabled(true)
			m.KeyMap.ClearFilter.SetEnabled(false)

		case key.Matches(msg, m.KeyMap.AcceptWhileFiltering):
			m.hideStatusMessage()

			if len(m.items) == 0 {
				break
			}

			h := m.VisibleItems()

			// If we've filtered down to nothing, clear the filter
			if len(h) == 0 {
				m.resetFiltering()
				break
			}

			m.FilterInput.Blur()
			m.filterState = FilterApplied
			m.updateKeybindings()

			if m.FilterInput.Value() == "" {
				m.resetFiltering()
			}
		}
	}

	// Update the filter text input component
	newFilterInputModel, inputCmd := m.FilterInput.Update(msg)
	filterChanged := m.FilterInput.Value() != newFilterInputModel.Value()
	m.FilterInput = newFilterInputModel
	cmds = append(cmds, inputCmd)

	// If the filtering input has changed, request updated filtering
	if filterChanged {
		cmds = append(cmds, filterItems(*m))
		m.KeyMap.AcceptWhileFiltering.SetEnabled(m.FilterInput.Value() != "")
	}

	// Update pagination
	m.updatePagination()

	return tea.Batch(cmds...)
}

// ShortHelp returns bindings to show in the abbreviated help view. It's part
// of the help.KeyMap interface.
func (m Model) ShortHelp() []key.Binding {
	kb := []key.Binding{
		m.KeyMap.CursorUp,
		m.KeyMap.CursorDown,
	}

	filtering := m.filterState == Filtering

	// If the delegate implements the help.KeyMap interface add the short help
	// items to the short help after the cursor movement keys.
	if !filtering {
		if b, ok := m.delegate.(help.KeyMap); ok {
			kb = append(kb, b.ShortHelp()...)
		}
	}

	kb = append(kb,
		m.KeyMap.Filter,
		m.KeyMap.ClearFilter,
		m.KeyMap.AcceptWhileFiltering,
		m.KeyMap.CancelWhileFiltering,
	)

	if !filtering && m.AdditionalShortHelpKeys != nil {
		kb = append(kb, m.AdditionalShortHelpKeys()...)
	}

	return append(kb,
		m.KeyMap.Quit,
		m.KeyMap.ShowFullHelp,
	)
}

// FullHelp returns bindings to show the full help view. It's part of the
// help.KeyMap interface.
func (m Model) FullHelp() [][]key.Binding {
	kb := [][]key.Binding{{
		m.KeyMap.CursorUp,
		m.KeyMap.CursorDown,
		m.KeyMap.NextPage,
		m.KeyMap.PrevPage,
		m.KeyMap.GoToStart,
		m.KeyMap.GoToEnd,
	}}

	filtering := m.filterState == Filtering

	// If the delegate implements the help.KeyMap interface add full help
	// keybindings to a special section of the full help.
	if !filtering {
		if b, ok := m.delegate.(help.KeyMap); ok {
			kb = append(kb, b.FullHelp()...)
		}
	}

	listLevelBindings := []key.Binding{
		m.KeyMap.Filter,
		m.KeyMap.ClearFilter,
		m.KeyMap.AcceptWhileFiltering,
		m.KeyMap.CancelWhileFiltering,
	}

	if !filtering && m.AdditionalFullHelpKeys != nil {
		listLevelBindings = append(listLevelBindings, m.AdditionalFullHelpKeys()...)
	}

	return append(kb,
		listLevelBindings,
		[]key.Binding{
			m.KeyMap.Quit,
			m.KeyMap.CloseFullHelp,
		})
}

// View renders the component.
func (m Model) View() string {
	var (
		sections    []string
		availHeight = m.height
	)

	if m.showTitle || (m.showFilter && m.filteringEnabled) {
		v := m.titleView()
		sections = append(sections, v)
		availHeight -= lipgloss.Height(v)
	}

	if m.showStatusBar {
		v := m.statusView()
		sections = append(sections, v)
		availHeight -= lipgloss.Height(v)
	}

	var pagination string
	if m.showPagination {
		pagination = m.paginationView()
		availHeight -= lipgloss.Height(pagination)
	}

	var help string
	if m.showHelp {
		help = m.helpView()
		availHeight -= lipgloss.Height(help)
	}

	content := lipgloss.NewStyle().Height(availHeight).Render(m.populatedView())
	sections = append(sections, content)

	if m.showPagination {
		sections = append(sections, pagination)
	}

	if m.showHelp {
		sections = append(sections, help)
	}

	return lipgloss.JoinVertical(lipgloss.Left, sections...)
}

func (m Model) titleView() string {
	var (
		view          string
		titleBarStyle = m.Styles.TitleBar

		// We need to account for the size of the spinner, even if we don't
		// render it, to reserve some space for it should we turn it on later.
		spinnerView    = m.spinnerView()
		spinnerWidth   = lipgloss.Width(spinnerView)
		spinnerLeftGap = " "
		spinnerOnLeft  = titleBarStyle.GetPaddingLeft() >= spinnerWidth+lipgloss.Width(spinnerLeftGap) && m.showSpinner
	)

	// If the filter's showing, draw that. Otherwise draw the title.
	if m.showFilter && m.filterState == Filtering {
		view += m.FilterInput.View()
	} else if m.showTitle {
		if m.showSpinner && spinnerOnLeft {
			view += spinnerView + spinnerLeftGap
			titleBarGap := titleBarStyle.GetPaddingLeft()
			titleBarStyle = titleBarStyle.PaddingLeft(titleBarGap - spinnerWidth - lipgloss.Width(spinnerLeftGap))
		}

		view += m.Styles.Title.Render(m.Title)

		// Status message
		if m.filterState != Filtering {
			view += "  " + m.statusMessage
			view = ansi.Truncate(view, m.width-spinnerWidth, ellipsis)
		}
	}

	// Spinner
	if m.showSpinner && !spinnerOnLeft {
		// Place spinner on the right
		availSpace := m.width - lipgloss.Width(m.Styles.TitleBar.Render(view))
		if availSpace > spinnerWidth {
			view += strings.Repeat(" ", availSpace-spinnerWidth)
			view += spinnerView
		}
	}

	if len(view) > 0 {
		return titleBarStyle.Render(view)
	}
	return view
}

func (m Model) statusView() string {
	var status string

	totalItems := len(m.items)
	visibleItems := len(m.VisibleItems())

	var itemName string
	if visibleItems != 1 {
		itemName = m.itemNamePlural
	} else {
		itemName = m.itemNameSingular
	}

	itemsDisplay := fmt.Sprintf("%d %s", visibleItems, itemName)

	if m.filterState == Filtering { //nolint:nestif
		// Filter results
		if visibleItems == 0 {
			status = m.Styles.StatusEmpty.Render("Nothing matched")
		} else {
			status = itemsDisplay
		}
	} else if len(m.items) == 0 {
		// Not filtering: no items.
		status = m.Styles.StatusEmpty.Render("No " + m.itemNamePlural)
	} else {
		// Normal
		filtered := m.FilterState() == FilterApplied

		if filtered {
			f := strings.TrimSpace(m.FilterInput.Value())
			f = ansi.Truncate(f, 10, "…") //nolint:mnd
			status += fmt.Sprintf("“%s” ", f)
		}

		status += itemsDisplay
	}

	numFiltered := totalItems - visibleItems
	if numFiltered > 0 {
		status += m.Styles.DividerDot.String()
		status += m.Styles.StatusBarFilterCount.Render(fmt.Sprintf("%d filtered", numFiltered))
	}

	return m.Styles.StatusBar.Render(status)
}

func (m Model) paginationView() string {
	if m.Paginator.TotalPages < 2 { //nolint:mnd
		return ""
	}

	s := m.Paginator.View()

	// If the dot pagination is wider than the width of the window
	// use the arabic paginator.
	if ansi.StringWidth(s) > m.width {
		m.Paginator.Type = paginator.Arabic
		s = m.Styles.ArabicPagination.Render(m.Paginator.View())
	}

	style := m.Styles.PaginationStyle
	if m.delegate.Spacing() == 0 && style.GetMarginTop() == 0 {
		style = style.MarginTop(1)
	}

	return style.Render(s)
}

func (m Model) populatedView() string {
	items := m.VisibleItems()

	var b strings.Builder

	// Empty states
	if len(items) == 0 {
		if m.filterState == Filtering {
			return ""
		}
		return m.Styles.NoItems.Render("No " + m.itemNamePlural + ".")
	}

	if len(items) > 0 {
		start, end := m.Paginator.GetSliceBounds(len(items))
		docs := items[start:end]

		for i, item := range docs {
			m.delegate.Render(&b, m, i+start, item)
			if i != len(docs)-1 {
				fmt.Fprint(&b, strings.Repeat("\n", m.delegate.Spacing()+1))
			}
		}
	}

	// If there aren't enough items to fill up this page (always the last page)
	// then we need to add some newlines to fill up the space where items would
	// have been.
	itemsOnPage := m.Paginator.ItemsOnPage(len(items))
	if itemsOnPage < m.Paginator.PerPage {
		n := (m.Paginator.PerPage - itemsOnPage) * (m.delegate.Height() + m.delegate.Spacing())
		if len(items) == 0 {
			n -= m.delegate.Height() - 1
		}
		fmt.Fprint(&b, strings.Repeat("\n", n))
	}

	return b.String()
}

func (m Model) helpView() string {
	return m.Styles.HelpStyle.Render(m.Help.View(m))
}

func (m Model) spinnerView() string {
	return m.spinner.View()
}

func filterItems(m Model) tea.Cmd {
	return func() tea.Msg {
		if m.FilterInput.Value() == "" || m.filterState == Unfiltered {
			return FilterMatchesMsg(m.itemsAsFilterItems()) // return nothing
		}

		items := m.items
		targets := make([]string, len(items))

		for i, t := range items {
			targets[i] = t.FilterValue()
		}

		filterMatches := []filteredItem{}
		for _, r := range m.Filter(m.FilterInput.Value(), targets) {
			filterMatches = append(filterMatches, filteredItem{
				index:   r.Index,
				item:    items[r.Index],
				matches: r.MatchedIndexes,
			})
		}

		return FilterMatchesMsg(filterMatches)
	}
}

func insertItemIntoSlice(items []Item, item Item, index int) []Item {
	if items == nil {
		return []Item{item}
	}
	if index >= len(items) {
		return append(items, item)
	}

	index = max(0, index)

	items = append(items, nil)
	copy(items[index+1:], items[index:])
	items[index] = item
	return items
}

// Remove an item from a slice of items at the given index. This runs in O(n).
func removeItemFromSlice(i []Item, index int) []Item {
	if index >= len(i) {
		return i // noop
	}
	copy(i[index:], i[index+1:])
	i[len(i)-1] = nil
	return i[:len(i)-1]
}

func removeFilterMatchFromSlice(i []filteredItem, index int) []filteredItem {
	if index >= len(i) {
		return i // noop
	}
	copy(i[index:], i[index+1:])
	i[len(i)-1] = filteredItem{}
	return i[:len(i)-1]
}

func countEnabledBindings(groups [][]key.Binding) (agg int) {
	for _, group := range groups {
		for _, kb := range group {
			if kb.Enabled() {
				agg++
			}
		}
	}
	return agg
}


================================================
FILE: list/list_test.go
================================================
package list

import (
	"fmt"
	"io"
	"reflect"
	"slices"
	"strings"
	"testing"

	tea "charm.land/bubbletea/v2"
)

type item string

func (i item) FilterValue() string { return string(i) }

type itemDelegate struct{}

func (d itemDelegate) Height() int                          { return 1 }
func (d itemDelegate) Spacing() int                         { return 0 }
func (d itemDelegate) Update(msg tea.Msg, m *Model) tea.Cmd { return nil }
func (d itemDelegate) Render(w io.Writer, m Model, index int, listItem Item) {
	i, ok := listItem.(item)
	if !ok {
		return
	}

	str := fmt.Sprintf("%d. %s", index+1, i)
	fmt.Fprint(w, m.Styles.TitleBar.Render(str))
}

func TestStatusBarItemName(t *testing.T) {
	list := New([]Item{item("foo"), item("bar")}, itemDelegate{}, 10, 10)
	expected := "2 items"
	if !strings.Contains(list.statusView(), expected) {
		t.Fatalf("Error: expected view to contain %s", expected)
	}

	list.SetItems([]Item{item("foo")})
	expected = "1 item"
	if !strings.Contains(list.statusView(), expected) {
		t.Fatalf("Error: expected view to contain %s", expected)
	}
}

func TestStatusBarWithoutItems(t *testing.T) {
	list := New([]Item{}, itemDelegate{}, 10, 10)

	expected := "No items"
	if !strings.Contains(list.statusView(), expected) {
		t.Fatalf("Error: expected view to contain %s", expected)
	}
}

func TestCustomStatusBarItemName(t *testing.T) {
	list := New([]Item{item("foo"), item("bar")}, itemDelegate{}, 10, 10)
	list.SetStatusBarItemName("connection", "connections")

	expected := "2 connections"
	if !strings.Contains(list.statusView(), expected) {
		t.Fatalf("Error: expected view to contain %s", expected)
	}

	list.SetItems([]Item{item("foo")})
	expected = "1 connection"
	if !strings.Contains(list.statusView(), expected) {
		t.Fatalf("Error: expected view to contain %s", expected)
	}

	list.SetItems([]Item{})
	expected = "No connections"
	if !strings.Contains(list.statusView(), expected) {
		t.Fatalf("Error: expected view to contain %s", expected)
	}
}

func TestSetFilterText(t *testing.T) {
	tc := []Item{item("foo"), item("bar"), item("baz")}

	list := New(tc, itemDelegate{}, 10, 10)
	list.SetFilterText("ba")

	list.SetFilterState(Unfiltered)
	expected := tc
	if !slices.Equal(list.VisibleItems(), expected) {
		t.Fatalf("Error: expected view to contain only %s", expected)
	}

	list.SetFilterState(Filtering)
	expected = []Item{item("bar"), item("baz")}
	if !reflect.DeepEqual(list.VisibleItems(), expected) {
		t.Fatalf("Error: expected view to contain only %s", expected)
	}

	list.SetFilterState(FilterApplied)
	if !reflect.DeepEqual(list.VisibleItems(), expected) {
		t.Fatalf("Error: expected view to contain only %s", expected)
	}
}

func TestSetFilterState(t *testing.T) {
	tc := []Item{item("foo"), item("bar"), item("baz")}

	list := New(tc, itemDelegate{}, 10, 10)
	list.SetFilterText("ba")

	list.SetFilterState(Unfiltered)
	expected, notExpected := "up", "clear filter"

	lines := strings.Split(list.View(), "\n")
	footer := lines[len(lines)-1]

	if !strings.Contains(footer, expected) || strings.Contains(footer, notExpected) {
		t.Fatalf("Error: expected view to contain '%s' not '%s'", expected, notExpected)
	}

	list.SetFilterState(Filtering)
	expected, notExpected = "filter", "more"

	lines = strings.Split(list.View(), "\n")
	footer = lines[len(lines)-1]

	if !strings.Contains(footer, expected) || strings.Contains(footer, notExpected) {
		t.Fatalf("Error: expected view to contain '%s' not '%s'", expected, notExpected)
	}

	list.SetFilterState(FilterApplied)
	expected = "clear"

	lines = strings.Split(list.View(), "\n")
	footer = lines[len(lines)-1]

	if !strings.Contains(footer, expected) {
		t.Fatalf("Error: expected view to contain '%s'", expected)
	}
}


================================================
FILE: list/style.go
================================================
package list

import (
	"charm.land/bubbles/v2/textinput"
	"charm.land/lipgloss/v2"
)

const (
	bullet   = "•"
	ellipsis = "…"
)

// Styles contains style definitions for this list component. By default, these
// values are generated by DefaultStyles.
type Styles struct {
	TitleBar lipgloss.Style
	Title    lipgloss.Style
	Spinner  lipgloss.Style
	Filter   textinput.Styles

	// Default styling for matched characters in a filter. This can be
	// overridden by delegates.
	DefaultFilterCharacterMatch lipgloss.Style

	StatusBar             lipgloss.Style
	StatusEmpty           lipgloss.Style
	StatusBarActiveFilter lipgloss.Style
	StatusBarFilterCount  lipgloss.Style

	NoItems lipgloss.Style

	PaginationStyle lipgloss.Style
	HelpStyle       lipgloss.Style

	// Styled characters.
	ActivePaginationDot   lipgloss.Style
	InactivePaginationDot lipgloss.Style
	ArabicPagination      lipgloss.Style
	DividerDot            lipgloss.Style
}

// DefaultStyles returns a set of default style definitions for this list
// component.
func DefaultStyles(isDark bool) (s Styles) {
	lightDark := lipgloss.LightDark(isDark)

	verySubduedColor := lightDark(lipgloss.Color("#DDDADA"), lipgloss.Color("#3C3C3C"))
	subduedColor := lightDark(lipgloss.Color("#9B9B9B"), lipgloss.Color("#5C5C5C"))

	s.TitleBar = lipgloss.NewStyle().Padding(0, 0, 1, 2) //nolint:mnd

	s.Title = lipgloss.NewStyle().
		Background(lipgloss.Color("62")).
		Foreground(lipgloss.Color("230")).
		Padding(0, 1)

	s.Spinner = lipgloss.NewStyle().
		Foreground(lightDark(lipgloss.Color("#8E8E8E"), lipgloss.Color("#747373")))

	prompt := lipgloss.NewStyle().
		Foreground(lightDark(lipgloss.Color("#04B575"), lipgloss.Color("#ECFD65")))
	s.Filter = textinput.DefaultStyles(isDark)
	s.Filter.Cursor.Color = lightDark(lipgloss.Color("#EE6FF8"), lipgloss.Color("#EE6FF8"))
	s.Filter.Blurred.Prompt = prompt
	s.Filter.Focused.Prompt = prompt

	s.DefaultFilterCharacterMatch = lipgloss.NewStyle().Underline(true)

	s.StatusBar = lipgloss.NewStyle().
		Foreground(lightDark(lipgloss.Color("#A49FA5"), lipgloss.Color("#777777"))).
		Padding(0, 0, 1, 2) //nolint:mnd

	s.StatusEmpty = lipgloss.NewStyle().Foreground(subduedColor)

	s.StatusBarActiveFilter = lipgloss.NewStyle().
		Foreground(lightDark(lipgloss.Color("#1a1a1a"), lipgloss.Color("#dddddd")))

	s.StatusBarFilterCount = lipgloss.NewStyle().Foreground(verySubduedColor)

	s.NoItems = lipgloss.NewStyle().
		Foreground(lightDark(lipgloss.Color("#909090"), lipgloss.Color("#626262")))

	s.ArabicPagination = lipgloss.NewStyle().Foreground(subduedColor)

	s.PaginationStyle = lipgloss.NewStyle().PaddingLeft(2) //nolint:mnd

	s.HelpStyle = lipgloss.NewStyle().Padding(1, 0, 0, 2) //nolint:mnd

	s.ActivePaginationDot = lipgloss.NewStyle().
		Foreground(lightDark(lipgloss.Color("#847A85"), lipgloss.Color("#979797"))).
		SetString(bullet)

	s.InactivePaginationDot = lipgloss.NewStyle().
		Foreground(verySubduedColor).
		SetString(bullet)

	s.DividerDot = lipgloss.NewStyle().
		Foreground(verySubduedColor).
		SetString(" " + bullet + " ")

	return s
}


================================================
FILE: paginator/paginator.go
================================================
// Package paginator provides a Bubble Tea package for calculating pagination
// and rendering pagination info. Note that this package does not render actual
// pages: it's purely for handling keystrokes related to pagination, and
// rendering pagination status.
package paginator

import (
	"fmt"

	"charm.land/bubbles/v2/key"
	tea "charm.land/bubbletea/v2"
)

// Type specifies the way we render pagination.
type Type int

// Pagination rendering options.
const (
	Arabic Type = iota
	Dots
)

// KeyMap is the key bindings for different actions within the paginator.
type KeyMap struct {
	PrevPage key.Binding
	NextPage key.Binding
}

// DefaultKeyMap is the default set of key bindings for navigating and acting
// upon the paginator.
func DefaultKeyMap() KeyMap {
	return KeyMap{
		PrevPage: key.NewBinding(key.WithKeys("pgup", "left", "h")),
		NextPage: key.NewBinding(key.WithKeys("pgdown", "right", "l")),
	}
}

// Model is the Bubble Tea model for this user interface.
type Model struct {
	// Type configures how the pagination is rendered (Arabic, Dots).
	Type Type
	// Page is the current page number.
	Page int
	// PerPage is the number of items per page.
	PerPage int
	// TotalPages is the total number of pages.
	TotalPages int
	// ActiveDot is used to mark the current page under the Dots display type.
	ActiveDot string
	// InactiveDot is used to mark inactive pages under the Dots display type.
	InactiveDot string
	// ArabicFormat is the printf-style format to use for the Arabic display type.
	ArabicFormat string

	// KeyMap encodes the keybindings recognized by the widget.
	KeyMap KeyMap
}

// SetTotalPages is a helper function for calculating the total number of pages
// from a given number of items. Its use is optional since this pager can be
// used for other things beyond navigating sets. Note that it both returns the
// number of total pages and alters the model.
func (m *Model) SetTotalPages(items int) int {
	if items < 1 {
		return m.TotalPages
	}
	n := items / m.PerPage
	if items%m.PerPage > 0 {
		n++
	}
	m.TotalPages = n
	return n
}

// ItemsOnPage is a helper function for returning the number of items on the
// current page given the total number of items passed as an argument.
func (m Model) ItemsOnPage(totalItems int) int {
	if totalItems < 1 {
		return 0
	}
	start, end := m.GetSliceBounds(totalItems)
	return end - start
}

// GetSliceBounds is a helper function for paginating slices. Pass the length
// of the slice you're rendering and you'll receive the start and end bounds
// corresponding to the pagination. For example:
//
//	bunchOfStuff := []stuff{...}
//	start, end := model.GetSliceBounds(len(bunchOfStuff))
//	sliceToRender := bunchOfStuff[start:end]
func (m *Model) GetSliceBounds(length int) (start int, end int) {
	start = m.Page * m.PerPage
	end = min(m.Page*m.PerPage+m.PerPage, length)
	return start, end
}

// PrevPage is a helper function for navigating one page backward. It will not
// page beyond the first page (i.e. page 0).
func (m *Model) PrevPage() {
	if m.Page > 0 {
		m.Page--
	}
}

// NextPage is a helper function for navigating one page forward. It will not
// page beyond the last page (i.e. totalPages - 1).
func (m *Model) NextPage() {
	if !m.OnLastPage() {
		m.Page++
	}
}

// OnLastPage returns whether or not we're on the last page.
func (m Model) OnLastPage() bool {
	return m.Page == m.TotalPages-1
}

// OnFirstPage returns whether or not we're on the first page.
func (m Model) OnFirstPage() bool {
	return m.Page == 0
}

// Option is used to set options in New.
type Option func(*Model)

// New creates a new model with defaults.
func New(opts ...Option) Model {
	m := Model{
		Type:         Arabic,
		Page:         0,
		PerPage:      1,
		TotalPages:   1,
		KeyMap:       DefaultKeyMap(),
		ActiveDot:    "•",
		InactiveDot:  "○",
		ArabicFormat: "%d/%d",
	}

	for _, opt := range opts {
		opt(&m)
	}

	return m
}

// WithTotalPages sets the total pages.
func WithTotalPages(totalPages int) Option {
	return func(m *Model) {
		m.TotalPages = totalPages
	}
}

// WithPerPage sets the total pages.
func WithPerPage(perPage int) Option {
	return func(m *Model) {
		m.PerPage = perPage
	}
}

// Update is the Tea update function which binds keystrokes to pagination.
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.KeyPressMsg:
		switch {
		case key.Matches(msg, m.KeyMap.NextPage):
			m.NextPage()
		case key.Matches(msg, m.KeyMap.PrevPage):
			m.PrevPage()
		}
	}

	return m, nil
}

// View renders the pagination to a string.
func (m Model) View() string {
	switch m.Type { //nolint:exhaustive
	case Dots:
		return m.dotsView()
	default:
		return m.arabicView()
	}
}

func (m Model) dotsView() string {
	var s string
	for i := range m.TotalPages {
		if i == m.Page {
			s += m.ActiveDot
			continue
		}
		s += m.InactiveDot
	}
	return s
}

func (m Model) arabicView() string {
	return fmt.Sprintf(m.ArabicFormat, m.Page+1, m.TotalPages)
}


================================================
FILE: paginator/paginator_test.go
================================================
package paginator

import (
	"testing"

	tea "charm.land/bubbletea/v2"
)

func TestNew(t *testing.T) {
	model := New()

	if model.PerPage != 1 {
		t.Errorf("PerPage = %d, expected %d", model.PerPage, 1)
	}
	if model.TotalPages != 1 {
		t.Errorf("TotalPages = %d, expected %d", model.TotalPages, 1)
	}

	perPage := 42
	totalPages := 42

	model = New(
		WithPerPage(perPage),
		WithTotalPages(totalPages),
	)

	if model.PerPage != perPage {
		t.Errorf("PerPage = %d, expected %d", model.PerPage, perPage)
	}
	if model.TotalPages != totalPages {
		t.Errorf("TotalPages = %d, expected %d", model.TotalPages, totalPages)
	}
}

func TestSetTotalPages(t *testing.T) {
	tests := []struct {
		name         string
		items        int // total no of items to be set
		initialTotal int // intital total pages for the testcase
		expected     int // expected value after SetTotalPages function call
	}{
		{"Less than one page", 5, 1, 5},
		{"Exactly one page", 10, 1, 10},
		{"More than one page", 15, 1, 15},
		{"negative value for page", -10, 1, 1},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			model := New()
			if model.TotalPages != tt.initialTotal {
				model.SetTotalPages(tt.initialTotal)
			}
			model.SetTotalPages(tt.items)
			if model.TotalPages != tt.expected {
				t.Errorf("TotalPages = %d, expected %d", model.TotalPages, tt.expected)
			}
		})
	}
}

func TestPrevPage(t *testing.T) {
	tests := []struct {
		name       string
		totalPages int // Total pages to be set for the testcase
		page       int // intital page for test
		expected   int
	}{
		{"Go to previous page", 10, 1, 0},
		{"Stay on first page", 5, 0, 0},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			model := New()
			model.SetTotalPages(tt.totalPages)
			model.Page = tt.page

			model, _ = model.Update(tea.KeyPressMsg{Code: tea.KeyLeft})
			if model.Page != tt.expected {
				t.Errorf("PrevPage() = %d, expected %d", model.Page, tt.expected)
			}
		})
	}
}

func TestNextPage(t *testing.T) {
	tests := []struct {
		name       string
		totalPages int
		page       int
		expected   int
	}{
		{"Go to next page", 2, 0, 1},
		{"Stay on last page", 2, 1, 1},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			model := New()
			model.SetTotalPages(tt.totalPages)
			model.Page = tt.page

			model, _ = model.Update(tea.KeyPressMsg{Code: tea.KeyRight})
			if model.Page != tt.expected {
				t.Errorf("NextPage() = %d, expected %d", model.Page, tt.expected)
			}
		})
	}
}

func TestOnLastPage(t *testing.T) {
	tests := []struct {
		name       string
		page       int
		totalPages int
		expected   bool
	}{
		{"On last page", 1, 2, true},
		{"Not on last page", 0, 2, false},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			model := New()
			model.SetTotalPages(tt.totalPages)
			model.Page = tt.page

			if result := model.OnLastPage(); result != tt.expected {
				t.Errorf("OnLastPage() = %t, expected %t", result, tt.expected)
			}
		})
	}
}

func TestOnFirstPage(t *testing.T) {
	tests := []struct {
		name       string
		page       int
		totalPages int
		expected   bool
	}{
		{"On first page", 0, 2, true},
		{"Not on first page", 1, 2, false},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			model := New()
			model.SetTotalPages(tt.totalPages)
			model.Page = tt.page

			if result := model.OnFirstPage(); result != tt.expected {
				t.Errorf("OnFirstPage() = %t, expected %t", result, tt.expected)
			}
		})
	}
}

func TestItemsOnPage(t *testing.T) {
	testCases := []struct {
		currentPage   int // current page to be set for the testcase
		totalPages    int // Total pages to be set for the testcase
		totalItems    int // Total items
		expectedItems int // expected items on current page
	}{
		{1, 10, 10, 1},
		{3, 10, 10, 1},
		{7, 10, 10, 1},
	}

	for _, tc := range testCases {
		model := New()
		model.Page = tc.currentPage
		model.SetTotalPages(tc.totalPages)
		if actualItems := model.ItemsOnPage(tc.totalItems); actualItems != tc.expectedItems {
			t.Errorf("ItemsOnPage() returned %d, expected %d for total items %d", actualItems, tc.expectedItems, tc.totalItems)
		}
	}
}


================================================
FILE: progress/progress.go
================================================
// Package progress provides a simple progress bar for Bubble Tea applications.
package progress

import (
	"fmt"
	"image/color"
	"math"
	"strings"
	"sync/atomic"
	"time"

	tea "charm.land/bubbletea/v2"
	"charm.land/lipgloss/v2"
	"github.com/charmbracelet/harmonica"
	"github.com/charmbracelet/x/ansi"
)

// ColorFunc is a function that can be used to dynamically fill the progress
// bar based on the current percentage. total is the total filled percentage,
// and current is the current percentage that is actively being filled with a
// color.
type ColorFunc func(total, current float64) color.Color

// Internal ID management. Used during animating to assure that frame messages
// can only be received by progress components that sent them.
var lastID int64

func nextID() int {
	return int(atomic.AddInt64(&lastID, 1))
}

const (
	// DefaultFullCharHalfBlock is the default character used to fill the progress
	// bar. It is a half block, which allows more granular color blending control,
	// by having a different foreground and background color, doubling blending
	// resolution.
	DefaultFullCharHalfBlock = '▌'

	// DefaultFullCharFullBlock can also be used as a fill character for the
	// progress bar. Use this to disable the higher resolution blending which is
	// enabled when using [DefaultFullCharHalfBlock].
	DefaultFullCharFullBlock = '█'

	// DefaultEmptyCharBlock is the default character used to fill the empty
	// portion of the progress bar.
	DefaultEmptyCharBlock = '░'

	fps              = 60
	defaultWidth     = 40
	defaultFrequency = 18.0
	defaultDamping   = 1.0
)

var (
	defaultBlendStart = lipgloss.Color("#5A56E0") // Purple haze.
	defaultBlendEnd   = lipgloss.Color("#EE6FF8") // Neon pink.
	defaultFullColor  = lipgloss.Color("#7571F9") // Blueberry.
	defaultEmptyColor = lipgloss.Color("#606060") // Slate gray.
)

// Option is used to set options in [New]. For example:
//
//	progress := New(
//		WithColors(
//			lipgloss.Color("#5A56E0"),
//			lipgloss.Color("#EE6FF8"),
//		),
//		WithoutPercentage(),
//	)
type Option func(*Model)

// WithDefaultBlend sets a default blend of colors, which is a blend of purple
// haze to neon pink.
func WithDefaultBlend() Option {
	return WithColors(
		defaultBlendStart,
		defaultBlendEnd,
	)
}

// WithColors sets the colors to use to fill the progress bar. Depending on the
// number of colors passed in, will determine whether to use a solid fill or a
// blend of colors.
//
//   - 0 colors: clears all previously set colors, setting them back to defaults.
//   - 1 color: uses a solid fill with the given color.
//   - 2+ colors: uses a blend of the provided colors.
func WithColors(colors ...color.Color) Option {
	if len(colors) == 0 {
		return func(m *Model) {
			m.FullColor = defaultFullColor
			m.blend = nil
			m.colorFunc = nil
		}
	}
	if len(colors) == 1 {
		return func(m *Model) {
			m.FullColor = colors[0]
			m.colorFunc = nil
			m.blend = nil
		}
	}
	return func(m *Model) {
		m.blend = colors
	}
}

// WithColorFunc sets a function that can be used to dynamically fill the progress
// bar based on the current percentage. total is the total filled percentage, and
// current is the current percentage that is actively being filled with a color.
// When specified, this overrides any other defined colors and scaling.
//
// Example: A progress bar that changes color based on the total completed
// percentage:
//
//	WithColorFunc(func(total, current float64) color.Color {
//		if total <= 0.3 {
//			return lipgloss.Color("#FF0000")
//		}
//		if total <= 0.7 {
//			return lipgloss.Color("#00FF00")
//		}
//		return lipgloss.Color("#0000FF")
//	}),
func WithColorFunc(fn ColorFunc) Option {
	return func(m *Model) {
		m.colorFunc = fn
		m.blend = nil
	}
}

// WithFillCharacters sets the characters used to construct the full and empty
// components of the progress bar.
func WithFillCharacters(full rune, empty rune) Option {
	return func(m *Model) {
		m.Full = full
		m.Empty = empty
	}
}

// WithoutPercentage hides the numeric percentage.
func WithoutPercentage() Option {
	return func(m *Model) {
		m.ShowPercentage = false
	}
}

// WithWidth sets the initial width of the progress bar. Note that you can also
// set the width via the Width property, which can come in handy if you're
// waiting for a tea.WindowSizeMsg.
func WithWidth(w int) Option {
	return func(m *Model) {
		m.SetWidth(w)
	}
}

// WithSpringOptions sets the initial frequency and damping options for the
// progress bar's built-in spring-based animation. Frequency corresponds to
// speed, and damping to bounciness. For details see:
//
// https://github.com/charmbracelet/harmonica
func WithSpringOptions(frequency, damping float64) Option {
	return func(m *Model) {
		m.SetSpringOptions(frequency, damping)
		m.springCustomized = true
	}
}

// WithScaled sets whether to scale the blend/gradient to fit the width of only
// the filled portion of the progress bar. The default is false, which means the
// percentage must be 100% to see the full color blend/gradient.
//
// This is ignored when not using blending/multiple colors.
func WithScaled(enabled bool) Option {
	return func(m *Model) {
		m.scaleBlend = enabled
	}
}

// FrameMsg indicates that an animation step should occur.
type FrameMsg struct {
	id  int
	tag int
}

// Model stores values we'll use when rendering the progress bar.
type Model struct {
	// An identifier to keep us from receiving messages intended for other
	// progress bars.
	id int

	// An identifier to keep us from receiving frame messages too quickly.
	tag int

	// Total width of the progress bar, including percentage, if set.
	width int

	// "Filled" sections of the progress bar.
	Full      rune
	FullColor color.Color

	// "Empty" sections of the progress bar.
	Empty      rune
	EmptyColor color.Color

	// Settings for rendering the numeric percentage.
	ShowPercentage  bool
	PercentFormat   string // a fmt string for a float
	PercentageStyle lipgloss.Style

	// Members for animated transitions.
	spring           harmonica.Spring
	springCustomized bool
	percentShown     float64 // percent currently displaying
	targetPercent    float64 // percent to which we're animating
	velocity         float64

	// Blend of colors to use. When len < 1, we use FullColor.
	blend []color.Color

	// When true, we scale the blended colors to fit the width of the filled
	// section of the progress bar. When false, the width of the blend will be
	// set to the full width of the progress bar.
	scaleBlend bool

	// colorFunc is used to dynamically fill the progress bar based on the
	// current percentage.
	colorFunc ColorFunc
}

// New returns a model with default values.
func New(opts ...Option) Model {
	m := Model{
		id:             nextID(),
		width:          defaultWidth,
		Full:           DefaultFullCharHalfBlock,
		FullColor:      defaultFullColor,
		Empty:          DefaultEmptyCharBlock,
		EmptyColor:     defaultEmptyColor,
		ShowPercentage: true,
		PercentFormat:  " %3.0f%%",
	}

	for _, opt := range opts {
		opt(&m)
	}

	if !m.springCustomized {
		m.SetSpringOptions(defaultFrequency, defaultDamping)
	}

	return m
}

// Init exists to satisfy the tea.Model interface.
func (m Model) Init() tea.Cmd {
	return nil
}

// Update is used to animate the progress bar during transitions. Use
// SetPercent to create the command you'll need to trigger the animation.
//
// If you're rendering with ViewAs you won't need this.
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
	switch msg := msg.(type) {
	case FrameMsg:
		if msg.id != m.id || msg.tag != m.tag {
			return m, nil
		}

		// If we've more or less reached equilibrium, stop updating.
		if !m.IsAnimating() {
			return m, nil
		}

		m.percentShown, m.velocity = m.spring.Update(m.percentShown, m.velocity, m.targetPercent)
		return m, m.nextFrame()

	default:
		return m, nil
	}
}

// SetSpringOptions sets the frequency and damping for the current spring.
// Frequency corresponds to speed, and damping to bounciness. For details see:
//
// https://github.com/charmbracelet/harmonica
func (m *Model) SetSpringOptions(frequency, damping float64) {
	m.spring = harmonica.NewSpring(harmonica.FPS(fps), frequency, damping)
}

// Percent returns the current visible percentage on the model. This is only
// relevant when you're animating the progress bar.
//
// If you're rendering with ViewAs you won't need this.
func (m Model) Percent() float64 {
	return m.targetPercent
}

// SetPercent sets the percentage state of the model as well as a command
// necessary for animating the progress bar to this new percentage.
//
// If you're rendering with ViewAs you won't need this.
func (m *Model) SetPercent(p float64) tea.Cmd {
	m.targetPercent = math.Max(0, math.Min(1, p))
	m.tag++
	return m.nextFrame()
}

// IncrPercent increments the percentage by a given amount, returning a command
// necessary to animate the progress bar to the new percentage.
//
// If you're rendering with ViewAs you won't need this.
func (m *Model) IncrPercent(v float64) tea.Cmd {
	return m.SetPercent(m.Percent() + v)
}

// DecrPercent decrements the percentage by a given amount, returning a command
// necessary to animate the progress bar to the new percentage.
//
// If you're rendering with ViewAs you won't need this.
func (m *Model) DecrPercent(v float64) tea.Cmd {
	return m.SetPercent(m.Percent() - v)
}

// View renders an animated progress bar in its current state. To render
// a static progress bar based on your own calculations use ViewAs instead.
func (m Model) View() string {
	return m.ViewAs(m.percentShown)
}

// ViewAs renders the progress bar with a given percentage.
func (m Model) ViewAs(percent float64) string {
	b := strings.Builder{}
	percentView := m.percentageView(percent)
	m.barView(&b, percent, ansi.StringWidth(percentView))
	b.WriteString(percentView)
	return b.String()
}

// SetWidth sets the width of the progress bar.
func (m *Model) SetWidth(w int) {
	m.width = w
}

// Width returns the width of the progress bar.
func (m Model) Width() int {
	return m.width
}

func (m *Model) nextFrame() tea.Cmd {
	return tea.Tick(time.Second/time.Duration(fps), func(time.Time) tea.Msg {
		return FrameMsg{id: m.id, tag: m.tag}
	})
}

func (m Model) barView(b *strings.Builder, percent float64, textWidth int) {
	var (
		tw = max(0, m.width-textWidth)                // total width
		fw = int(math.Round((float64(tw) * percent))) // filled width
	)

	fw = max(0, min(tw, fw))

	isHalfBlock := m.Full == DefaultFullCharHalfBlock

	if m.colorFunc != nil { //nolint:nestif
		var style lipgloss.Style
		var current float64
		halfBlockPerc := 0.5 / float64(tw)
		for i := range fw {
			current = float64(i) / float64(tw)
			style = style.Foreground(m.colorFunc(percent, current))
			if isHalfBlock {
				style = style.Background(m.colorFunc(percent, min(current+halfBlockPerc, 1)))
			}
			b.WriteString(style.Render(string(m.Full)))
		}
	} else if len(m.blend) > 0 {
		var blend []color.Color

		multiplier := 1
		if isHalfBlock {
			multiplier = 2
		}

		if m.scaleBlend {
			blend = lipgloss.Blend1D(fw*multiplier, m.blend...)
		} else {
			blend = lipgloss.Blend1D(tw*multiplier, m.blend...)
		}

		// Blend fill.
		var blendIndex int
		for i := range fw {
			if !isHalfBlock {
				b.WriteString(lipgloss.NewStyle().
					Foreground(blend[i]).
					Render(string(m.Full)))
				continue
			}

			b.WriteString(lipgloss.NewStyle().
				Foreground(blend[blendIndex]).
				Background(blend[blendIndex+1]).
				Render(string(m.Full)))
			blendIndex += 2
		}
	} else {
		// Solid fill.
		b.WriteString(lipgloss.NewStyle().
			Foreground(m.FullColor).
			Render(strings.Repeat(string(m.Full), fw)))
	}

	// Empty fill.
	n := max(0, tw-fw)
	b.WriteString(lipgloss.NewStyle().
		Foreground(m.EmptyColor).
		Render(strings.Repeat(string(m.Empty), n)))
}

func (m Model) percentageView(percent float64) string {
	if !m.ShowPercentage {
		return ""
	}
	percent = math.Max(0, math.Min(1, percent))
	percentage := fmt.Sprintf(m.PercentFormat, percent*100) //nolint:mnd
	percentage = m.PercentageStyle.Inline(true).Render(percentage)
	return percentage
}

// IsAnimating returns false if the progress bar reached equilibrium and is no
// longer animating.
func (m *Model) IsAnimating() bool {
	dist := math.Abs(m.percentShown - m.targetPercent)
	return !(dist < 0.001 && m.velocity < 0.01)
}


================================================
FILE: progress/progress_test.go
================================================
package progress

import (
	"image/color"
	"testing"

	"charm.land/lipgloss/v2"
	"github.com/charmbracelet/x/exp/golden"
)

func TestBlend(t *testing.T) {
	tests := []struct {
		name    string
		options []Option
		width   int
		percent float64
	}{
		{
			name: "10w-red-to-green-50perc",
			options: []Option{
				WithColors(lipgloss.Color("#FF0000"), lipgloss.Color("#00FF00")),
				WithScaled(false),
				WithoutPercentage(),
			},
			width:   10,
			percent: 0.5,
		},
		{
			name: "10w-red-to-green-50perc-full-block",
			options: []Option{
				WithColors(lipgloss.Color("#FF0000"), lipgloss.Color("#00FF00")),
				WithFillCharacters('█', DefaultEmptyCharBlock),
				WithoutPercentage(),
			},
			width:   10,
			percent: 0.5,
		},
		{
			name: "30w-red-to-green-100perc",
			options: []Option{
				WithColors(lipgloss.Color("#FF0000"), lipgloss.Color("#00FF00")),
				WithScaled(false),
				WithoutPercentage(),
			},
			width:   30,
			percent: 1.0,
		},
		{
			name: "10w-red-to-green-scaled-50perc",
			options: []Option{
				WithColors(lipgloss.Color("#FF0000"), lipgloss.Color("#00FF00")),
				WithScaled(true),
				WithoutPercentage(),
			},
			width:   10,
			percent: 0.5,
		},
		{
			name: "30w-red-to-green-scaled-100perc",
			options: []Option{
				WithColors(lipgloss.Color("#FF0000"), lipgloss.Color("#00FF00")),
				WithScaled(true),
				WithoutPercentage(),
			},
			width:   30,
			percent: 1.0,
		},
		{
			name: "30w-colorfunc-rgb-100perc",
			options: []Option{
				WithColorFunc(func(_, current float64) color.Color {
					if current <= 0.3 {
						return lipgloss.Color("#FF0000")
					}
					if current <= 0.7 {
						return lipgloss.Color("#00FF00")
					}
					return lipgloss.Color("#0000FF")
				}),
				WithoutPercentage(),
			},
			width:   30,
			percent: 1.0,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			p := New(tt.options...)
			p.SetWidth(tt.width)
			golden.RequireEqual(t, []byte(p.ViewAs(tt.percent)))
		})
	}
}


================================================
FILE: progress/testdata/TestBlend/10w-red-to-green-50perc-full-block.golden
================================================
█████░░░░░

================================================
FILE: progress/testdata/TestBlend/10w-red-to-green-50perc.golden
================================================
▌▌▌▌▌░░░░░

================================================
FILE: progress/testdata/TestBlend/10w-red-to-green-scaled-50perc.golden
================================================
▌▌▌▌▌░░░░░

================================================
FILE: progress/testdata/TestBlend/30w-colorfunc-rgb-100perc.golden
================================================
▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌

================================================
FILE: progress/testdata/TestBlend/30w-red-to-green-100perc.golden
================================================
▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌

================================================
FILE: progress/testdata/TestBlend/30w-red-to-green-scaled-100perc.golden
================================================
▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌

================================================
FILE: spinner/spinner.go
================================================
// Package spinner provides a spinner component for Bubble Tea applications.
package spinner

import (
	"sync/atomic"
	"time"

	tea "charm.land/bubbletea/v2"
	"charm.land/lipgloss/v2"
)

// Internal ID management. Used during animating to ensure that frame messages
// are received only by spinner components that sent them.
var lastID int64

func nextID() int {
	return int(atomic.AddInt64(&lastID, 1))
}

// Spinner is a set of frames used in animating the spinner.
type Spinner struct {
	Frames []string
	FPS    time.Duration
}

// Some spinners to choose from. You could also make your own.
var (
	Line = Spinner{
		Frames: []string{"|", "/", "-", "\\"},
		FPS:    time.Second / 10, //nolint:mnd
	}
	Dot = Spinner{
		Frames: []string{"⣾ ", "⣽ ", "⣻ ", "⢿ ", "⡿ ", "⣟ ", "⣯ ", "⣷ "},
		FPS:    time.Second / 10, //nolint:mnd
	}
	MiniDot = Spinner{
		Frames: []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"},
		FPS:    time.Second / 12, //nolint:mnd
	}
	Jump = Spinner{
		Frames: []string{"⢄", "⢂", "⢁", "⡁", "⡈", "⡐", "⡠"},
		FPS:    time.Second / 10, //nolint:mnd
	}
	Pulse = Spinner{
		Frames: []string{"█", "▓", "▒", "░"},
		FPS:    time.Second / 8, //nolint:mnd
	}
	Points = Spinner{
		Frames: []string{"∙∙∙", "●∙∙", "∙●∙", "∙∙●"},
		FPS:    time.Second / 7, //nolint:mnd
	}
	Globe = Spinner{
		Frames: []string{"🌍", "🌎", "🌏"},
		FPS:    time.Second / 4, //nolint:mnd
	}
	Moon = Spinner{
		Frames: []string{"🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘"},
		FPS:    time.Second / 8, //nolint:mnd
	}
	Monkey = Spinner{
		Frames: []string{"🙈", "🙉", "🙊"},
		FPS:    time.Second / 3, //nolint:mnd
	}
	Meter = Spinner{
		Frames: []string{
			"▱▱▱",
			"▰▱▱",
			"▰▰▱",
			"▰▰▰",
			"▰▰▱",
			"▰▱▱",
			"▱▱▱",
		},
		FPS: time.Second / 7, //nolint:mnd
	}
	Hamburger = Spinner{
		Frames: []string{"☱", "☲", "☴", "☲"},
		FPS:    time.Second / 3, //nolint:mnd
	}
	Ellipsis = Spinner{
		Frames: []string{"", ".", "..", "..."},
		FPS:    time.Second / 3, //nolint:mnd
	}
)

// Model contains the state for the spinner. Use New to create new models
// rather than using Model as a struct literal.
type Model struct {
	// Spinner settings to use. See type Spinner.
	Spinner Spinner

	// Style sets the styling for the spinner. Most of the time you'll just
	// want foreground and background coloring, and potentially some padding.
	//
	// For an introduction to styling with Lip Gloss see:
	// https://github.com/charmbracelet/lipgloss
	Style lipgloss.Style

	frame int
	id    int
	tag   int
}

// ID returns the spinner's unique ID.
func (m Model) ID() int {
	return m.id
}

// New returns a model with default values.
func New(opts ...Option) Model {
	m := Model{
		Spinner: Line,
		id:      nextID(),
	}

	for _, opt := range opts {
		opt(&m)
	}

	return m
}

// TickMsg indicates that the timer has ticked and we should render a frame.
type TickMsg struct {
	Time time.Time
	tag  int
	ID   int
}

// Update is the Tea update function.
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
	switch msg := msg.(type) {
	case TickMsg:
		// If an ID is set, and the ID doesn't belong to this spinner, reject
		// the message.
		if msg.ID > 0 && msg.ID != m.id {
			return m, nil
		}

		// If a tag is set, and it's not the one we expect, reject the message.
		// This prevents the spinner from receiving too many messages and
		// thus spinning too fast.
		if msg.tag > 0 && msg.tag != m.tag {
			return m, nil
		}

		m.frame++
		if m.frame >= len(m.Spinner.Frames) {
			m.frame = 0
		}

		m.tag++
		return m, m.tick(m.id, m.tag)
	default:
		return m, nil
	}
}

// View renders the model's view.
func (m Model) View() string {
	if m.frame >= len(m.Spinner.Frames) {
		return "(error)"
	}

	return m.Style.Render(m.Spinner.Frames[m.frame])
}

// Tick is the command used to advance the spinner one frame. Use this command
// to effectively start the spinner.
func (m Model) Tick() tea.Msg {
	return TickMsg{
		// The time at which the tick occurred.
		Time: time.Now(),

		// The ID of the spinner that this message belongs to. This can be
		// helpful when routing messages, however bear in mind that spinners
		// will ignore messages that don't contain ID by default.
		ID: m.id,

		tag: m.tag,
	}
}

func (m Model) tick(id, tag int) tea.Cmd {
	return tea.Tick(m.Spinner.FPS, func(t time.Time) tea.Msg {
		return TickMsg{
			Time: t,
			ID:   id,
			tag:  tag,
		}
	})
}

// Option is used to set options in New. For example:
//
//	spinner := New(WithSpinner(Dot))
type Option func(*Model)

// WithSpinner is an option to set the spinner. Pass this to [Spinner.New].
func WithSpinner(spinner Spinner) Option {
	return func(m *Model) {
		m.Spinner = spinner
	}
}

// WithStyle is an option to set the spinner style. Pass this to [Spinner.New].
func WithStyle(style lipgloss.Style) Option {
	return func(m *Model) {
		m.Style = style
	}
}


================================================
FILE: spinner/spinner_test.go
================================================
package spinner_test

import (
	"testing"

	"charm.land/bubbles/v2/spinner"
)

func TestSpinnerNew(t *testing.T) {
	assertEqualSpinner := func(t *testing.T, exp, got spinner.Spinner) {
		t.Helper()

		if exp.FPS != got.FPS {
			t.Errorf("expecting %d FPS, got %d", exp.FPS, got.FPS)
		}

		if e, g := len(exp.Frames), len(got.Frames); e != g {
			t.Fatalf("expecting %d frames, got %d", e, g)
		}

		for i, e := range exp.Frames {
			if g := got.Frames[i]; e != g {
				t.Errorf("expecting frame index %d with value %q, got %q", i, e, g)
			}
		}
	}
	t.Run("default", func(t *testing.T) {
		s := spinner.New()

		assertEqualSpinner(t, spinner.Line, s.Spinner)
	})

	t.Run("WithSpinner", func(t *testing.T) {
		customSpinner := spinner.Spinner{
			Frames: []string{"a", "b", "c", "d"},
			FPS:    16,
		}

		s := spinner.New(spinner.WithSpinner(customSpinner))

		assertEqualSpinner(t, customSpinner, s.Spinner)
	})

	tests := map[string]spinner.Spinner{
		"Line":    spinner.Line,
		"Dot":     spinner.Dot,
		"MiniDot": spinner.MiniDot,
		"Jump":    spinner.Jump,
		"Pulse":   spinner.Pulse,
		"Points":  spinner.Points,
		"Globe":   spinner.Globe,
		"Moon":    spinner.Moon,
		"Monkey":  spinner.Monkey,
	}

	for name, s := range tests {
		t.Run(name, func(t *testing.T) {
			assertEqualSpinner(t, spinner.New(spinner.WithSpinner(s)).Spinner, s)
		})
	}
}


================================================
FILE: stopwatch/stopwatch.go
================================================
// Package stopwatch provides a simple stopwatch component.
package stopwatch

import (
	"sync/atomic"
	"time"

	tea "charm.land/bubbletea/v2"
)

var lastID int64

func nextID() int {
	return int(atomic.AddInt64(&lastID, 1))
}

// Option is a configuration option in [New]. For example:
//
//	timer := New(time.Second*10, WithInterval(5*time.Second))
type Option func(*Model)

// WithInterval is an option for setting the interval between ticks. Pass as
// an argument to [New].
func WithInterval(interval time.Duration) Option {
	return func(m *Model) {
		m.Interval = interval
	}
}

// TickMsg is a message that is sent on every timer tick.
type TickMsg struct {
	// ID is the identifier of the stopwatch that sends the message. This makes
	// it possible to determine which stopwatch a tick belongs to when there
	// are multiple stopwatches running.
	//
	// Note, however, that a stopwatch will reject ticks from other
	// stopwatches, so it's safe to flow all TickMsgs through all stopwatches
	// and have them still behave appropriately.
	ID  int
	tag int
}

// StartStopMsg is sent when the stopwatch should start or stop.
type StartStopMsg struct {
	ID      int
	running bool
}

// ResetMsg is sent when the stopwatch should reset.
type ResetMsg struct {
	ID int
}

// Model for the stopwatch component.
type Model struct {
	d       time.Duration
	id      int
	tag     int
	running bool

	// How long to wait before every tick. Defaults to 1 second.
	Interval time.Duration
}

// New creates a new stopwatch with 1s interval.
func New(opts ...Option) Model {
	m := Model{
		id: nextID(),
	}

	for _, opt := range opts {
		opt(&m)
	}
	return m
}

// ID returns the unique ID of the model.
func (m Model) ID() int {
	return m.id
}

// Init starts the stopwatch.
func (m Model) Init() tea.Cmd {
	return m.Start()
}

// Start starts the stopwatch.
func (m Model) Start() tea.Cmd {
	return tea.Sequence(func() tea.Msg {
		return StartStopMsg{ID: m.id, running: true}
	}, tick(m.id, m.tag, m.Interval))
}

// Stop stops the stopwatch.
func (m Model) Stop() tea.Cmd {
	return func() tea.Msg {
		return StartStopMsg{ID: m.id, running: false}
	}
}

// Toggle stops the stopwatch if it is running and starts it if it is stopped.
func (m Model) Toggle() tea.Cmd {
	if m.Running() {
		return m.Stop()
	}
	return m.Start()
}

// Reset resets the stopwatch to 0.
func (m Model) Reset() tea.Cmd {
	return func() tea.Msg {
		return ResetMsg{ID: m.id}
	}
}

// Running returns true if the stopwatch is running or false if it is stopped.
func (m Model) Running() bool {
	return m.running
}

// Update handles the timer tick.
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
	switch msg := msg.(type) {
	case StartStopMsg:
		if msg.ID != m.id {
			return m, nil
		}
		m.running = msg.running
	case ResetMsg:
		if msg.ID != m.id {
			return m, nil
		}
		m.d = 0
	case TickMsg:
		if !m.running || msg.ID != m.id {
			break
		}

		// If a tag is set, and it's not the one we expect, reject the message.
		// This prevents the stopwatch from receiving too many messages and
		// thus ticking too fast.
		if msg.tag > 0 && msg.tag != m.tag {
			return m, nil
		}

		m.d += m.Interval
		m.tag++
		return m, tick(m.id, m.tag, m.Interval)
	}

	return m, nil
}

// Elapsed returns the time elapsed.
func (m Model) Elapsed() time.Duration {
	return m.d
}

// View of the timer component.
func (m Model) View() string {
	return m.d.String()
}

func tick(id int, tag int, d time.Duration) tea.Cmd {
	return tea.Tick(d, func(_ time.Time) tea.Msg {
		return TickMsg{ID: id, tag: tag}
	})
}


================================================
FILE: table/table.go
================================================
// Package table provides a simple table component for Bubble Tea applications.
package table

import (
	"strings"

	"charm.land/bubbles/v2/help"
	"charm.land/bubbles/v2/key"
	"charm.land/bubbles/v2/viewport"
	tea "charm.land/bubbletea/v2"
	"charm.land/lipgloss/v2"
	"github.com/charmbracelet/x/ansi"
)

// Model defines a state for the table widget.
type Model struct {
	KeyMap KeyMap
	Help   help.Model

	cols   []Column
	rows   []Row
	cursor int
	focus  bool
	styles Styles

	viewport viewport.Model
	start    int
	end      int
}

// Row represents one line in the table.
type Row []string

// Column defines the table structure.
type Column struct {
	Title string
	Width int
}

// KeyMap defines keybindings. It satisfies to the help.KeyMap interface, which
// is used to render the help menu.
type KeyMap struct {
	LineUp       key.Binding
	LineDown     key.Binding
	PageUp       key.Binding
	PageDown     key.Binding
	HalfPageUp   key.Binding
	HalfPageDown key.Binding
	GotoTop      key.Binding
	GotoBottom   key.Binding
}

// ShortHelp implements the KeyMap interface.
func (km KeyMap) ShortHelp() []key.Binding {
	return []key.Binding{km.LineUp, km.LineDown}
}

// FullHelp implements the KeyMap interface.
func (km KeyMap) FullHelp() [][]key.Binding {
	return [][]key.Binding{
		{km.LineUp, km.LineDown, km.GotoTop, km.GotoBottom},
		{km.PageUp, km.PageDown, km.HalfPageUp, km.HalfPageDown},
	}
}

// DefaultKeyMap returns a default set of keybindings.
func DefaultKeyMap() KeyMap {
	return KeyMap{
		LineUp: key.NewBinding(
			key.WithKeys("up", "k"),
			key.WithHelp("↑/k", "up"),
		),
		LineDown: key.NewBinding(
			key.WithKeys("down", "j"),
			key.WithHelp("↓/j", "down"),
		),
		PageUp: key.NewBinding(
			key.WithKeys("b", "pgup"),
			key.WithHelp("b/pgup", "page up"),
		),
		PageDown: key.NewBinding(
			key.WithKeys("f", "pgdown", "space"),
			key.WithHelp("f/pgdn", "page down"),
		),
		HalfPageUp: key.NewBinding(
			key.WithKeys("u", "ctrl+u"),
			key.WithHelp("u", "½ page up"),
		),
		HalfPageDown: key.NewBinding(
			key.WithKeys("d", "ctrl+d"),
			key.WithHelp("d", "½ page down"),
		),
		GotoTop: key.NewBinding(
			key.WithKeys("home", "g"),
			key.WithHelp("g/home", "go to start"),
		),
		GotoBottom: key.NewBinding(
			key.WithKeys("end", "G"),
			key.WithHelp("G/end", "go to end"),
		),
	}
}

// Styles contains style definitions for this list component. By default, these
// values are generated by DefaultStyles.
type Styles struct {
	Header   lipgloss.Style
	Cell     lipgloss.Style
	Selected lipgloss.Style
}

// DefaultStyles returns a set of default style definitions for this table.
func DefaultStyles() Styles {
	return Styles{
		Selected: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("212")),
		Header:   lipgloss.NewStyle().Bold(true).Padding(0, 1),
		Cell:     lipgloss.NewStyle().Padding(0, 1),
	}
}

// SetStyles sets the table styles.
func (m *Model) SetStyles(s Styles) {
	m.styles = s
	m.UpdateViewport()
}

// Option is used to set options in New. For example:
//
//	table := New(WithColumns([]Column{{Title: "ID", Width: 10}}))
type Option func(*Model)

// New creates a new model for the table widget.
func New(opts ...Option) Model {
	m := Model{
		cursor:   0,
		viewport: viewport.New(viewport.WithHeight(20)), //nolint:mnd

		KeyMap: DefaultKeyMap(),
		Help:   help.New(),
		styles: DefaultStyles(),
	}

	for _, opt := range opts {
		opt(&m)
	}

	m.UpdateViewport()

	return m
}

// WithColumns sets the table columns (headers).
func WithColumns(cols []Column) Option {
	return func(m *Model) {
		m.cols = cols
	}
}

// WithRows sets the table rows (data).
func WithRows(rows []Row) Option {
	return func(m *Model) {
		m.rows = rows
	}
}

// WithHeight sets the height of the table.
func WithHeight(h int) Option {
	return func(m *Model) {
		m.viewport.SetHeight(h - lipgloss.Height(m.headersView()))
	}
}

// WithWidth sets the width of the table.
func WithWidth(w int) Option {
	return func(m *Model) {
		m.viewport.SetWidth(w)
	}
}

// WithFocused sets the focus state of the table.
func WithFocused(f bool) Option {
	return func(m *Model) {
		m.focus = f
	}
}

// WithStyles sets the table styles.
func WithStyles(s Styles) Option {
	return func(m *Model) {
		m.styles = s
	}
}

// WithKeyMap sets the key map.
func WithKeyMap(km KeyMap) Option {
	return func(m *Model) {
		m.KeyMap = km
	}
}

// Update is the Bubble Tea update loop.
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
	if !m.focus {
		return m, nil
	}

	switch msg := msg.(type) {
	case tea.KeyPressMsg:
		switch {
		case key.Matches(msg, m.KeyMap.LineUp):
			m.MoveUp(1)
		case key.Matches(msg, m.KeyMap.LineDown):
			m.MoveDown(1)
		case key.Matches(msg, m.KeyMap.PageUp):
			m.MoveUp(m.viewport.Height())
		case key.Matches(msg, m.KeyMap.PageDown):
			m.MoveDown(m.viewport.Height())
		case key.Matches(msg, m.KeyMap.HalfPageUp):
			m.MoveUp(m.viewport.Height() / 2) //nolint:mnd
		case key.Matches(msg, m.KeyMap.HalfPageDown):
			m.MoveDown(m.viewport.Height() / 2) //nolint:mnd
		case key.Matches(msg, m.KeyMap.GotoTop):
			m.GotoTop()
		case key.Matches(msg, m.KeyMap.GotoBottom):
			m.GotoBottom()
		}
	}

	return m, nil
}

// Focused returns the focus state of the table.
func (m Model) Focused() bool {
	return m.focus
}

// Focus focuses the table, allowing the user to move around the rows and
// interact.
func (m *Model) Focus() {
	m.focus = true
	m.UpdateViewport()
}

// Blur blurs the table, preventing selection or movement.
func (m *Model) Blur() {
	m.focus = false
	m.UpdateViewport()
}

// View renders the component.
func (m Model) View() string {
	return m.headersView() + "\n" + m.viewport.View()
}

// HelpView is a helper method for rendering the help menu from the keymap.
// Note that this view is not rendered by default and you must call it
// manually in your application, where applicable.
func (m Model) HelpView() string {
	return m.Help.View(m.KeyMap)
}

// UpdateViewport updates the list content based on the previously defined
// columns and rows.
func (m *Model) UpdateViewport() {
	renderedRows := make([]string, 0, len(m.rows))

	// Render only rows from: m.cursor-m.viewport.Height to: m.cursor+m.viewport.Height
	// Constant runtime, independent of number of rows in a table.
	// Limits the number of renderedRows to a maximum of 2*m.viewport.Height
	if m.cursor >= 0 {
		m.start = clamp(m.cursor-m.viewport.Height(), 0, m.cursor)
	} else {
		m.start = 0
	}
	m.end = clamp(m.cursor+m.viewport.Height(), m.cursor, len(m.rows))
	for i := m.start; i < m.end; i++ {
		renderedRows = append(renderedRows, m.renderRow(i))
	}

	m.viewport.SetContent(
		lipgloss.JoinVertical(lipgloss.Left, renderedRows...),
	)
}

// SelectedRow returns the selected row.
// You can cast it to your own implementation.
func (m Model) SelectedRow() Row {
	if m.cursor < 0 || m.cursor >= len(m.rows) {
		return nil
	}

	return m.rows[m.cursor]
}

// Rows returns the current rows.
func (m Model) Rows() []Row {
	return m.rows
}

// Columns returns the current columns.
func (m Model) Columns() []Column {
	return m.cols
}

// SetRows sets a new rows state.
func (m *Model) SetRows(r []Row) {
	m.rows = r

	if m.cursor > len(m.rows)-1 {
		m.cursor = len(m.rows) - 1
	}

	m.UpdateViewport()
}

// SetColumns sets a new columns state.
func (m *Model) SetColumns(c []Column) {
	m.cols = c
	m.UpdateViewport()
}

// SetWidth sets the width of the viewport of the table.
func (m *Model) SetWidth(w int) {
	m.viewport.SetWidth(w)
	m.UpdateViewport()
}

// SetHeight sets the height of the viewport of the table.
func (m *Model) SetHeight(h int) {
	m.viewport.SetHeight(h - lipgloss.Height(m.headersView()))
	m.UpdateViewport()
}

// Height returns the viewport height of the table.
func (m Model) Height() int {
	return m.viewport.Height()
}

// Width returns the viewport width of the table.
func (m Model) Width() int {
	return m.viewport.Width()
}

// Cursor returns the index of the selected row.
func (m Model) Cursor() int {
	return m.cursor
}

// SetCursor sets the cursor position in the table.
func (m *Model) SetCursor(n int) {
	m.cursor = clamp(n, 0, len(m.rows)-1)
	m.UpdateViewport()
}

// MoveUp moves the selection up by any number of rows.
// It can not go above the first row.
func (m *Model) MoveUp(n int) {
	m.cursor = clamp(m.cursor-n, 0, len(m.rows)-1)

	offset := m.viewport.YOffset()
	switch {
	case m.start == 0:
		offset = clamp(offset, 0, m.cursor)
	case m.start < m.viewport.Height():
		offset = clamp(clamp(offset+n, 0, m.cursor), 0, m.viewport.Height())
	case offset >= 1:
		offset = clamp(offset+n, 1, m.viewport.Height())
	}
	m.viewport.SetYOffset(offset)
	m.UpdateViewport()
}

// MoveDown moves the selection down by any number of rows.
// It can not go below the last row.
func (m *Model) MoveDown(n int) {
	m.cursor = clamp(m.cursor+n, 0, len(m.rows)-1)
	m.UpdateViewport()

	offset := m.viewport.YOffset()
	switch {
	case m.end == len(m.rows) && offset > 0:
		offset = clamp(offset-n, 1, m.viewport.Height())
	case m.cursor > (m.end-m.start)/2 && offset > 0:
		offset = clamp(offset-n, 1, m.cursor)
	case offset > 1:
	case m.cursor > offset+m.viewport.Height()-1:
		offset = clamp(offset+1, 0, 1)
	}
	m.viewport.SetYOffset(offset)
}

// GotoTop moves the selection to the first row.
func (m *Model) GotoTop() {
	m.MoveUp(m.cursor)
}

// GotoBottom moves the selection to the last row.
func (m *Model) GotoBottom() {
	m.MoveDown(len(m.rows))
}

// FromValues create the table rows from a simple string. It uses `\n` by
// default for getting all the rows and the given separator for the fields on
// each row.
func (m *Model) FromValues(value, separator string) {
	rows := []Row{} //nolint:prealloc
	for _, line := range strings.Split(value, "\n") {
		r := Row{}
		for _, field := range strings.Split(line, separator) {
			r = append(r, field)
		}
		rows = append(rows, r)
	}

	m.SetRows(rows)
}

func (m Model) headersView() string {
	s := make([]string, 0, len(m.cols))
	for _, col := range m.cols {
		if col.Width <= 0 {
			continue
		}
		style := lipgloss.NewStyle().Width(col.Width).MaxWidth(col.Width).Inline(true)
		renderedCell := style.Render(ansi.Truncate(col.Title, col.Width, "…"))
		s = append(s, m.styles.Header.Render(renderedCell))
	}
	return lipgloss.JoinHorizontal(lipgloss.Top, s...)
}

func (m *Model) renderRow(r int) string {
	s := make([]string, 0, len(m.cols))
	for i, value := range m.rows[r] {
		if m.cols[i].Width <= 0 {
			continue
		}
		style := lipgloss.NewStyle().Width(m.cols[i].Width).MaxWidth(m.cols[i].Width).Inline(true)
		renderedCell := m.styles.Cell.Render(style.Render(ansi.Truncate(value, m.cols[i].Width, "…")))
		s = append(s, renderedCell)
	}

	row := lipgloss.JoinHorizontal(lipgloss.Top, s...)

	if r == m.cursor {
		return m.styles.Selected.Render(row)
	}

	return row
}

func clamp(v, low, high int) int {
	return min(max(v, low), high)
}


================================================
FILE: table/table_test.go
================================================
package table

import (
	"reflect"
	"strings"
	"testing"

	"charm.land/bubbles/v2/help"
	"charm.land/bubbles/v2/viewport"
	"charm.land/lipgloss/v2"
	"github.com/charmbracelet/x/ansi"
	"github.com/charmbracelet/x/exp/golden"
)

var testCols = []Column{
	{Title: "col1", Width: 10},
	{Title: "col2", Width: 10},
	{Title: "col3", Width: 10},
}

func TestNew(t *testing.T) {
	tests := map[string]struct {
		opts []Option
		want Model
	}{
		"Default": {
			want: Model{
				// Default fields
				cursor: 0,
				viewport: viewport.New(
					viewport.WithWidth(0),
					viewport.WithHeight(20),
				),
				KeyMap: DefaultKeyMap(),
				Help:   help.New(),
				styles: DefaultStyles(),
			},
		},
		"WithColumns": {
			opts: []Option{
				WithColumns([]Column{
					{Title: "Foo", Width: 1},
					{Title: "Bar", Width: 2},
				}),
			},
			want: Model{
				// Default fields
				cursor: 0,
				viewport: viewport.New(
					viewport.WithWidth(0),
					viewport.WithHeight(20),
				),
				KeyMap: DefaultKeyMap(),
				Help:   help.New(),
				styles: DefaultStyles(),

				// Modified fields
				cols: []Column{
					{Title: "Foo", Width: 1},
					{Title: "Bar", Width: 2},
				},
			},
		},
		"WithColumns; WithRows": {
			opts: []Option{
				WithColumns([]Column{
					{Title: "Foo", Width: 1},
					{Title: "Bar", Width: 2},
				}),
				WithRows([]Row{
					{"1", "Foo"},
					{"2", "Bar"},
				}),
			},
			want: Model{
				// Default fields
				cursor: 0,
				viewport: viewport.New(
					viewport.WithWidth(0),
					viewport.WithHeight(20),
				),
				KeyMap: DefaultKeyMap(),
				Help:   help.New(),
				styles: DefaultStyles(),

				// Modified fields
				cols: []Column{
					{Title: "Foo", Width: 1},
					{Title: "Bar", Width: 2},
				},
				rows: []Row{
					{"1", "Foo"},
					{"2", "Bar"},
				},
			},
		},
		"WithHeight": {
			opts: []Option{
				WithHeight(10),
			},
			want: Model{
				// Default fields
				cursor: 0,
				KeyMap: DefaultKeyMap(),
				Help:   help.New(),
				styles: DefaultStyles(),

				// Modified fields
				// Viewport height is 1 less than the provided height when no header is present since lipgloss.Height adds 1
				viewport: viewport.New(
					viewport.WithWidth(0),
					viewport.WithHeight(9),
				),
			},
		},
		"WithWidth": {
			opts: []Option{
				WithWidth(10),
			},
			want: Model{
				// Default fields
				cursor: 0,
				KeyMap: DefaultKeyMap(),
				Help:   help.New(),
				styles: DefaultStyles(),

				// Modified fields
				// Viewport height is 1 less than the provided height when no header is present since lipgloss.Height adds 1
				viewport: viewport.New(
					viewport.WithWidth(10),
					viewport.WithHeight(20),
				),
			},
		},
		"WithFocused": {
			opts: []Option{
				WithFocused(true),
			},
			want: Model{
				// Default fields
				cursor: 0,
				viewport: viewport.New(
					viewport.WithWidth(0),
					viewport.WithHeight(20),
				),
				KeyMap: DefaultKeyMap(),
				Help:   help.New(),
				styles: DefaultStyles(),

				// Modified fields
				focus: true,
			},
		},
		"WithStyles": {
			opts: []Option{
				WithStyles(Styles{}),
			},
			want: Model{
				// Default fields
				cursor: 0,
				viewport: viewport.New(
					viewport.WithWidth(0),
					viewport.WithHeight(20),
				),
				KeyMap: DefaultKeyMap(),
				Help:   help.New(),

				// Modified fields
				styles: Styles{},
			},
		},
		"WithKeyMap": {
			opts: []Option{
				WithKeyMap(KeyMap{}),
			},
			want: Model{
				// Default fields
				cursor: 0,
				viewport: viewport.New(
	
Download .txt
gitextract_bq3bte_v/

├── .github/
│   ├── CODEOWNERS
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   ├── config.yml
│   │   └── feature_request.md
│   ├── dependabot.yml
│   └── workflows/
│       ├── build.yml
│       ├── coverage.yml
│       ├── dependabot-sync.yml
│       ├── lint-sync.yml
│       ├── lint.yml
│       └── release.yml
├── .gitignore
├── .golangci.yml
├── .goreleaser.yml
├── LICENSE
├── README.md
├── Taskfile.yaml
├── UPGRADE_GUIDE_V2.md
├── bubbles.go
├── cursor/
│   ├── cursor.go
│   └── cursor_test.go
├── filepicker/
│   ├── filepicker.go
│   ├── hidden_unix.go
│   └── hidden_windows.go
├── go.mod
├── go.sum
├── help/
│   ├── help.go
│   ├── help_test.go
│   └── testdata/
│       └── TestFullHelp/
│           ├── full_help_20_width.golden
│           ├── full_help_30_width.golden
│           └── full_help_40_width.golden
├── internal/
│   ├── memoization/
│   │   ├── memoization.go
│   │   └── memoization_test.go
│   └── runeutil/
│       ├── runeutil.go
│       └── runeutil_test.go
├── key/
│   ├── key.go
│   └── key_test.go
├── list/
│   ├── README.md
│   ├── defaultitem.go
│   ├── keys.go
│   ├── list.go
│   ├── list_test.go
│   └── style.go
├── paginator/
│   ├── paginator.go
│   └── paginator_test.go
├── progress/
│   ├── progress.go
│   ├── progress_test.go
│   └── testdata/
│       └── TestBlend/
│           ├── 10w-red-to-green-50perc-full-block.golden
│           ├── 10w-red-to-green-50perc.golden
│           ├── 10w-red-to-green-scaled-50perc.golden
│           ├── 30w-colorfunc-rgb-100perc.golden
│           ├── 30w-red-to-green-100perc.golden
│           └── 30w-red-to-green-scaled-100perc.golden
├── spinner/
│   ├── spinner.go
│   └── spinner_test.go
├── stopwatch/
│   └── stopwatch.go
├── table/
│   ├── table.go
│   ├── table_test.go
│   └── testdata/
│       ├── TestModel_View/
│       │   ├── Bordered_cells.golden
│       │   ├── Bordered_headers.golden
│       │   ├── Empty.golden
│       │   ├── Extra_padding.golden
│       │   ├── Height_greater_than_rows.golden
│       │   ├── Height_less_than_rows.golden
│       │   ├── Modified_viewport_height.golden
│       │   ├── Multiple_rows_and_columns.golden
│       │   ├── No_padding.golden
│       │   ├── Single_row_and_column.golden
│       │   ├── Width_greater_than_columns.golden
│       │   └── Width_less_than_columns.golden
│       ├── TestModel_View_CenteredInABox.golden
│       └── TestTableAlignment/
│           ├── No_border.golden
│           └── With_border.golden
├── textarea/
│   ├── textarea.go
│   └── textarea_test.go
├── textinput/
│   ├── styles.go
│   ├── textinput.go
│   └── textinput_test.go
├── timer/
│   └── timer.go
└── viewport/
    ├── highlight.go
    ├── keymap.go
    ├── testdata/
    │   └── TestSizing/
    │       ├── view-40x1-softwrap-at-bottom.golden
    │       ├── view-40x1-softwrap-scrolled-plus-1.golden
    │       ├── view-40x1-softwrap-scrolled-plus-2.golden
    │       ├── view-40x1-softwrap.golden
    │       ├── view-40x1.golden
    │       ├── view-40x100percent.golden
    │       ├── view-50x15-content-lines.golden
    │       ├── view-50x15-softwrap-at-bottom.golden
    │       ├── view-50x15-softwrap-at-top.golden
    │       ├── view-50x15-softwrap-gutter-at-bottom.golden
    │       ├── view-50x15-softwrap-gutter-at-top.golden
    │       ├── view-50x15-softwrap-gutter-scrolled-plus-1.golden
    │       ├── view-50x15-softwrap-gutter-scrolled-plus-2.golden
    │       ├── view-50x15-softwrap-scrolled-plus-1.golden
    │       └── view-50x15-softwrap-scrolled-plus-2.golden
    ├── viewport.go
    └── viewport_test.go
Download .txt
SYMBOL INDEX (683 symbols across 37 files)

FILE: cursor/cursor.go
  constant defaultBlinkSpeed (line 14) | defaultBlinkSpeed = time.Millisecond * 530
  function nextID (line 20) | func nextID() int {
  type initialBlinkMsg (line 25) | type initialBlinkMsg struct
  type BlinkMsg (line 29) | type BlinkMsg struct
  type blinkCanceled (line 35) | type blinkCanceled struct
  type blinkCtx (line 38) | type blinkCtx struct
  type Mode (line 44) | type Mode
    method String (line 55) | func (c Mode) String() string {
  constant CursorBlink (line 48) | CursorBlink Mode = iota
  constant CursorStatic (line 49) | CursorStatic
  constant CursorHide (line 50) | CursorHide
  type Model (line 64) | type Model struct
    method Update (line 114) | func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
    method Mode (line 162) | func (m Model) Mode() Mode {
    method SetMode (line 169) | func (m *Model) SetMode(mode Mode) tea.Cmd {
    method Blink (line 183) | func (m *Model) Blink() tea.Cmd {
    method Focus (line 214) | func (m *Model) Focus() tea.Cmd {
    method Blur (line 225) | func (m *Model) Blur() {
    method SetChar (line 231) | func (m *Model) SetChar(char string) {
    method View (line 236) | func (m Model) View() string {
  function New (line 100) | func New() Model {
  function Blink (line 209) | func Blink() tea.Msg {

FILE: cursor/cursor_test.go
  function TestBlinkCmdDataRace (line 34) | func TestBlinkCmdDataRace(t *testing.T) {

FILE: filepicker/filepicker.go
  function nextID (line 22) | func nextID() int {
  function New (line 27) | func New() Model {
  type errorMsg (line 51) | type errorMsg struct
  type readDirMsg (line 55) | type readDirMsg struct
  constant marginBottom (line 61) | marginBottom  = 5
  constant fileSizeWidth (line 62) | fileSizeWidth = 7
  constant paddingLeft (line 63) | paddingLeft   = 2
  type KeyMap (line 67) | type KeyMap struct
  function DefaultKeyMap (line 80) | func DefaultKeyMap() KeyMap {
  type Styles (line 95) | type Styles struct
  function DefaultStyles (line 110) | func DefaultStyles() Styles {
  type Model (line 127) | type Model struct
    method pushView (line 187) | func (m *Model) pushView(selected, minimum, maximum int) {
    method popView (line 193) | func (m *Model) popView() (int, int, int) {
    method readDir (line 197) | func (m Model) readDir(path string, showHidden bool) tea.Cmd {
    method SetHeight (line 228) | func (m *Model) SetHeight(h int) {
    method Height (line 236) | func (m Model) Height() int {
    method Init (line 241) | func (m Model) Init() tea.Cmd {
    method Update (line 246) | func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
    method View (line 368) | func (m Model) View() string {
    method DidSelectFile (line 447) | func (m Model) DidSelectFile(msg tea.Msg) (bool, string) {
    method DidSelectDisabledFile (line 458) | func (m Model) DidSelectDisabledFile(msg tea.Msg) (bool, string) {
    method didSelectFile (line 466) | func (m Model) didSelectFile(msg tea.Msg) (bool, string) {
    method canSelect (line 510) | func (m Model) canSelect(file string) bool {
    method HighlightedPath (line 524) | func (m Model) HighlightedPath() string {
  type stack (line 164) | type stack struct
  function newStack (line 170) | func newStack() stack {

FILE: filepicker/hidden_unix.go
  function IsHidden (line 9) | func IsHidden(file string) (bool, error) {

FILE: filepicker/hidden_windows.go
  function IsHidden (line 11) | func IsHidden(file string) (bool, error) {

FILE: help/help.go
  type KeyMap (line 18) | type KeyMap interface
  type Styles (line 31) | type Styles struct
  function DefaultStyles (line 48) | func DefaultStyles(isDark bool) Styles {
  function DefaultDarkStyles (line 67) | func DefaultDarkStyles() Styles {
  function DefaultLightStyles (line 72) | func DefaultLightStyles() Styles {
  type Model (line 77) | type Model struct
    method Update (line 103) | func (m Model) Update(_ tea.Msg) (Model, tea.Cmd) {
    method View (line 108) | func (m Model) View(k KeyMap) string {
    method SetWidth (line 116) | func (m *Model) SetWidth(w int) {
    method Width (line 121) | func (m Model) Width() int {
    method ShortHelpView (line 128) | func (m Model) ShortHelpView(bindings []key.Binding) string {
    method FullHelpView (line 171) | func (m Model) FullHelpView(groups [][]key.Binding) string {
    method shouldAddItem (line 234) | func (m Model) shouldAddItem(totalWidth, width int) (tail string, ok b...
  function New (line 93) | func New() Model {
  function shouldRenderColumn (line 246) | func shouldRenderColumn(b []key.Binding) (ok bool) {

FILE: help/help_test.go
  function TestFullHelp (line 12) | func TestFullHelp(t *testing.T) {

FILE: internal/memoization/memoization.go
  type Hasher (line 14) | type Hasher interface
  type entry (line 20) | type entry struct
  type MemoCache (line 28) | type MemoCache struct
  function NewMemoCache (line 38) | func NewMemoCache[H Hasher, T any](capacity int) *MemoCache[H, T] {
  method Capacity (line 48) | func (m *MemoCache[H, T]) Capacity() int {
  method Size (line 54) | func (m *MemoCache[H, T]) Size() int {
  method Get (line 63) | func (m *MemoCache[H, T]) Get(h H) (T, bool) {
  method Set (line 79) | func (m *MemoCache[H, T]) Set(h H, value T) {
  type HString (line 112) | type HString
    method Hash (line 115) | func (h HString) Hash() string {
  type HInt (line 120) | type HInt
    method Hash (line 123) | func (h HInt) Hash() string {

FILE: internal/memoization/memoization_test.go
  type actionType (line 11) | type actionType
  constant set (line 14) | set actionType = iota
  constant get (line 15) | get
  type cacheAction (line 18) | type cacheAction struct
  type testCase (line 25) | type testCase struct
  function TestCache (line 31) | func TestCache(t *testing.T) {
  function FuzzCache (line 140) | func FuzzCache(f *testing.F) {

FILE: internal/runeutil/runeutil.go
  type Sanitizer (line 12) | type Sanitizer interface
  function NewSanitizer (line 24) | func NewSanitizer(opts ...Option) Sanitizer {
  type Option (line 36) | type Option
  function ReplaceTabs (line 39) | func ReplaceTabs(tabRepl string) Option {
  function ReplaceNewlines (line 47) | func ReplaceNewlines(nlRepl string) Option {
  type sanitizer (line 99) | type sanitizer struct
    method Sanitize (line 54) | func (s *sanitizer) Sanitize(runes []rune) []rune {

FILE: internal/runeutil/runeutil_test.go
  function TestSanitize (line 8) | func TestSanitize(t *testing.T) {

FILE: key/key.go
  type Binding (line 43) | type Binding struct
    method SetKeys (line 84) | func (b *Binding) SetKeys(keys ...string) {
    method Keys (line 89) | func (b Binding) Keys() []string {
    method SetHelp (line 94) | func (b *Binding) SetHelp(key, desc string) {
    method Help (line 99) | func (b Binding) Help() Help {
    method Enabled (line 106) | func (b Binding) Enabled() bool {
    method SetEnabled (line 111) | func (b *Binding) SetEnabled(v bool) {
    method Unbind (line 118) | func (b *Binding) Unbind() {
  type BindingOpt (line 51) | type BindingOpt
  function NewBinding (line 54) | func NewBinding(opts ...BindingOpt) Binding {
  function WithKeys (line 63) | func WithKeys(keys ...string) BindingOpt {
  function WithHelp (line 70) | func WithHelp(key, desc string) BindingOpt {
  function WithDisabled (line 77) | func WithDisabled() BindingOpt {
  type Help (line 124) | type Help struct
  function Matches (line 130) | func Matches[Key fmt.Stringer](k Key, b ...Binding) bool {

FILE: key/key_test.go
  function TestBinding_Enabled (line 7) | func TestBinding_Enabled(t *testing.T) {

FILE: list/defaultitem.go
  type DefaultItemStyles (line 16) | type DefaultItemStyles struct
  function NewDefaultItemStyles (line 35) | func NewDefaultItemStyles(isDark bool) (s DefaultItemStyles) {
  type DefaultItem (line 67) | type DefaultItem interface
  type DefaultDelegate (line 86) | type DefaultDelegate struct
    method SetHeight (line 111) | func (d *DefaultDelegate) SetHeight(i int) {
    method Height (line 118) | func (d DefaultDelegate) Height() int {
    method SetSpacing (line 126) | func (d *DefaultDelegate) SetSpacing(i int) {
    method Spacing (line 131) | func (d DefaultDelegate) Spacing() int {
    method Update (line 136) | func (d DefaultDelegate) Update(msg tea.Msg, m *Model) tea.Cmd {
    method Render (line 144) | func (d DefaultDelegate) Render(w io.Writer, m Model, index int, item ...
    method ShortHelp (line 220) | func (d DefaultDelegate) ShortHelp() []key.Binding {
    method FullHelp (line 228) | func (d DefaultDelegate) FullHelp() [][]key.Binding {
  function NewDefaultDelegate (line 97) | func NewDefaultDelegate() DefaultDelegate {

FILE: list/keys.go
  type KeyMap (line 7) | type KeyMap struct
  function DefaultKeyMap (line 34) | func DefaultKeyMap() KeyMap {

FILE: list/list.go
  function clamp (line 26) | func clamp[T cmp.Ordered](v, low, high T) T {
  type Item (line 34) | type Item interface
  type ItemDelegate (line 46) | type ItemDelegate interface
  type filteredItem (line 63) | type filteredItem struct
  type filteredItems (line 69) | type filteredItems
    method items (line 71) | func (f filteredItems) items() []Item {
  type FilterMatchesMsg (line 81) | type FilterMatchesMsg
  type FilterFunc (line 86) | type FilterFunc
  type Rank (line 89) | type Rank struct
  function DefaultFilter (line 98) | func DefaultFilter(term string, targets []string) []Rank {
  function UnsortedFilter (line 113) | func UnsortedFilter(term string, targets []string) []Rank {
  type statusMessageTimeoutMsg (line 125) | type statusMessageTimeoutMsg struct
  type FilterState (line 128) | type FilterState
    method String (line 138) | func (f FilterState) String() string {
  constant Unfiltered (line 132) | Unfiltered    FilterState = iota
  constant Filtering (line 133) | Filtering
  constant FilterApplied (line 134) | FilterApplied
  type Model (line 147) | type Model struct
    method SetFilteringEnabled (line 258) | func (m *Model) SetFilteringEnabled(v bool) {
    method FilteringEnabled (line 267) | func (m Model) FilteringEnabled() bool {
    method SetShowTitle (line 272) | func (m *Model) SetShowTitle(v bool) {
    method SetFilterText (line 280) | func (m *Model) SetFilterText(filter string) {
    method SetFilterState (line 295) | func (m *Model) SetFilterState(state FilterState) {
    method ShowTitle (line 304) | func (m Model) ShowTitle() bool {
    method SetShowFilter (line 314) | func (m *Model) SetShowFilter(v bool) {
    method ShowFilter (line 323) | func (m Model) ShowFilter() bool {
    method SetShowStatusBar (line 329) | func (m *Model) SetShowStatusBar(v bool) {
    method ShowStatusBar (line 335) | func (m Model) ShowStatusBar() bool {
    method SetStatusBarItemName (line 341) | func (m *Model) SetStatusBarItemName(singular, plural string) {
    method StatusBarItemName (line 347) | func (m Model) StatusBarItemName() (string, string) {
    method SetShowPagination (line 353) | func (m *Model) SetShowPagination(v bool) {
    method ShowPagination (line 359) | func (m *Model) ShowPagination() bool {
    method SetShowHelp (line 364) | func (m *Model) SetShowHelp(v bool) {
    method ShowHelp (line 370) | func (m Model) ShowHelp() bool {
    method Items (line 375) | func (m Model) Items() []Item {
    method SetItems (line 380) | func (m *Model) SetItems(i []Item) tea.Cmd {
    method Select (line 395) | func (m *Model) Select(index int) {
    method ResetSelected (line 401) | func (m *Model) ResetSelected() {
    method ResetFilter (line 406) | func (m *Model) ResetFilter() {
    method SetItem (line 411) | func (m *Model) SetItem(index int, item Item) tea.Cmd {
    method InsertItem (line 425) | func (m *Model) InsertItem(index int, item Item) tea.Cmd {
    method RemoveItem (line 441) | func (m *Model) RemoveItem(index int) {
    method SetDelegate (line 453) | func (m *Model) SetDelegate(d ItemDelegate) {
    method VisibleItems (line 459) | func (m Model) VisibleItems() []Item {
    method SelectedItem (line 467) | func (m Model) SelectedItem() Item {
    method MatchesForItem (line 482) | func (m Model) MatchesForItem(index int) []int {
    method Index (line 493) | func (m Model) Index() int {
    method GlobalIndex (line 499) | func (m Model) GlobalIndex() int {
    method Cursor (line 510) | func (m Model) Cursor() int {
    method CursorUp (line 516) | func (m *Model) CursorUp() {
    method CursorDown (line 542) | func (m *Model) CursorDown() {
    method GoToStart (line 568) | func (m *Model) GoToStart() {
    method GoToEnd (line 574) | func (m *Model) GoToEnd() {
    method PrevPage (line 580) | func (m *Model) PrevPage() {
    method NextPage (line 586) | func (m *Model) NextPage() {
    method maxCursorIndex (line 591) | func (m *Model) maxCursorIndex() int {
    method FilterState (line 596) | func (m Model) FilterState() FilterState {
    method FilterValue (line 601) | func (m Model) FilterValue() string {
    method SettingFilter (line 612) | func (m Model) SettingFilter() bool {
    method IsFiltered (line 620) | func (m Model) IsFiltered() bool {
    method Width (line 625) | func (m Model) Width() int {
    method Height (line 630) | func (m Model) Height() int {
    method SetSpinner (line 635) | func (m *Model) SetSpinner(spinner spinner.Spinner) {
    method ToggleSpinner (line 640) | func (m *Model) ToggleSpinner() tea.Cmd {
    method StartSpinner (line 649) | func (m *Model) StartSpinner() tea.Cmd {
    method StopSpinner (line 655) | func (m *Model) StopSpinner() {
    method DisableQuitKeybindings (line 661) | func (m *Model) DisableQuitKeybindings() {
    method NewStatusMessage (line 669) | func (m *Model) NewStatusMessage(s string) tea.Cmd {
    method SetWidth (line 685) | func (m *Model) SetWidth(v int) {
    method SetHeight (line 690) | func (m *Model) SetHeight(v int) {
    method SetSize (line 695) | func (m *Model) SetSize(width, height int) {
    method resetFiltering (line 706) | func (m *Model) resetFiltering() {
    method itemsAsFilterItems (line 718) | func (m Model) itemsAsFilterItems() filteredItems {
    method updateKeybindings (line 729) | func (m *Model) updateKeybindings() {
    method updatePagination (line 776) | func (m *Model) updatePagination() {
    method hideStatusMessage (line 811) | func (m *Model) hideStatusMessage() {
    method Update (line 819) | func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
    method handleBrowsing (line 853) | func (m *Model) handleBrowsing(msg tea.Msg) tea.Cmd {
    method handleFiltering (line 911) | func (m *Model) handleFiltering(msg tea.Msg) tea.Cmd {
    method ShortHelp (line 967) | func (m Model) ShortHelp() []key.Binding {
    method FullHelp (line 1002) | func (m Model) FullHelp() [][]key.Binding {
    method View (line 1042) | func (m Model) View() string {
    method titleView (line 1086) | func (m Model) titleView() string {
    method statusView (line 1134) | func (m Model) statusView() string {
    method paginationView (line 1181) | func (m Model) paginationView() string {
    method populatedView (line 1203) | func (m Model) populatedView() string {
    method helpView (line 1243) | func (m Model) helpView() string {
    method spinnerView (line 1247) | func (m Model) spinnerView() string {
  function New (line 207) | func New(items []Item, delegate ItemDelegate, width, height int) Model {
  function filterItems (line 1251) | func filterItems(m Model) tea.Cmd {
  function insertItemIntoSlice (line 1277) | func insertItemIntoSlice(items []Item, item Item, index int) []Item {
  function removeItemFromSlice (line 1294) | func removeItemFromSlice(i []Item, index int) []Item {
  function removeFilterMatchFromSlice (line 1303) | func removeFilterMatchFromSlice(i []filteredItem, index int) []filteredI...
  function countEnabledBindings (line 1312) | func countEnabledBindings(groups [][]key.Binding) (agg int) {

FILE: list/list_test.go
  type item (line 14) | type item
    method FilterValue (line 16) | func (i item) FilterValue() string { return string(i) }
  type itemDelegate (line 18) | type itemDelegate struct
    method Height (line 20) | func (d itemDelegate) Height() int                          { return 1 }
    method Spacing (line 21) | func (d itemDelegate) Spacing() int                         { return 0 }
    method Update (line 22) | func (d itemDelegate) Update(msg tea.Msg, m *Model) tea.Cmd { return n...
    method Render (line 23) | func (d itemDelegate) Render(w io.Writer, m Model, index int, listItem...
  function TestStatusBarItemName (line 33) | func TestStatusBarItemName(t *testing.T) {
  function TestStatusBarWithoutItems (line 47) | func TestStatusBarWithoutItems(t *testing.T) {
  function TestCustomStatusBarItemName (line 56) | func TestCustomStatusBarItemName(t *testing.T) {
  function TestSetFilterText (line 78) | func TestSetFilterText(t *testing.T) {
  function TestSetFilterState (line 102) | func TestSetFilterState(t *testing.T) {

FILE: list/style.go
  constant bullet (line 9) | bullet   = "•"
  constant ellipsis (line 10) | ellipsis = "…"
  type Styles (line 15) | type Styles struct
  function DefaultStyles (line 44) | func DefaultStyles(isDark bool) (s Styles) {

FILE: paginator/paginator.go
  type Type (line 15) | type Type
  constant Arabic (line 19) | Arabic Type = iota
  constant Dots (line 20) | Dots
  type KeyMap (line 24) | type KeyMap struct
  function DefaultKeyMap (line 31) | func DefaultKeyMap() KeyMap {
  type Model (line 39) | type Model struct
    method SetTotalPages (line 63) | func (m *Model) SetTotalPages(items int) int {
    method ItemsOnPage (line 77) | func (m Model) ItemsOnPage(totalItems int) int {
    method GetSliceBounds (line 92) | func (m *Model) GetSliceBounds(length int) (start int, end int) {
    method PrevPage (line 100) | func (m *Model) PrevPage() {
    method NextPage (line 108) | func (m *Model) NextPage() {
    method OnLastPage (line 115) | func (m Model) OnLastPage() bool {
    method OnFirstPage (line 120) | func (m Model) OnFirstPage() bool {
    method Update (line 162) | func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
    method View (line 177) | func (m Model) View() string {
    method dotsView (line 186) | func (m Model) dotsView() string {
    method arabicView (line 198) | func (m Model) arabicView() string {
  type Option (line 125) | type Option
  function New (line 128) | func New(opts ...Option) Model {
  function WithTotalPages (line 148) | func WithTotalPages(totalPages int) Option {
  function WithPerPage (line 155) | func WithPerPage(perPage int) Option {

FILE: paginator/paginator_test.go
  function TestNew (line 9) | func TestNew(t *testing.T) {
  function TestSetTotalPages (line 35) | func TestSetTotalPages(t *testing.T) {
  function TestPrevPage (line 62) | func TestPrevPage(t *testing.T) {
  function TestNextPage (line 87) | func TestNextPage(t *testing.T) {
  function TestOnLastPage (line 112) | func TestOnLastPage(t *testing.T) {
  function TestOnFirstPage (line 136) | func TestOnFirstPage(t *testing.T) {
  function TestItemsOnPage (line 160) | func TestItemsOnPage(t *testing.T) {

FILE: progress/progress.go
  type ColorFunc (line 22) | type ColorFunc
  function nextID (line 28) | func nextID() int {
  constant DefaultFullCharHalfBlock (line 37) | DefaultFullCharHalfBlock = '▌'
  constant DefaultFullCharFullBlock (line 42) | DefaultFullCharFullBlock = '█'
  constant DefaultEmptyCharBlock (line 46) | DefaultEmptyCharBlock = '░'
  constant fps (line 48) | fps              = 60
  constant defaultWidth (line 49) | defaultWidth     = 40
  constant defaultFrequency (line 50) | defaultFrequency = 18.0
  constant defaultDamping (line 51) | defaultDamping   = 1.0
  type Option (line 70) | type Option
  function WithDefaultBlend (line 74) | func WithDefaultBlend() Option {
  function WithColors (line 88) | func WithColors(colors ...color.Color) Option {
  function WithColorFunc (line 125) | func WithColorFunc(fn ColorFunc) Option {
  function WithFillCharacters (line 134) | func WithFillCharacters(full rune, empty rune) Option {
  function WithoutPercentage (line 142) | func WithoutPercentage() Option {
  function WithWidth (line 151) | func WithWidth(w int) Option {
  function WithSpringOptions (line 162) | func WithSpringOptions(frequency, damping float64) Option {
  function WithScaled (line 174) | func WithScaled(enabled bool) Option {
  type FrameMsg (line 181) | type FrameMsg struct
  type Model (line 187) | type Model struct
    method Init (line 256) | func (m Model) Init() tea.Cmd {
    method Update (line 264) | func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
    method SetSpringOptions (line 288) | func (m *Model) SetSpringOptions(frequency, damping float64) {
    method Percent (line 296) | func (m Model) Percent() float64 {
    method SetPercent (line 304) | func (m *Model) SetPercent(p float64) tea.Cmd {
    method IncrPercent (line 314) | func (m *Model) IncrPercent(v float64) tea.Cmd {
    method DecrPercent (line 322) | func (m *Model) DecrPercent(v float64) tea.Cmd {
    method View (line 328) | func (m Model) View() string {
    method ViewAs (line 333) | func (m Model) ViewAs(percent float64) string {
    method SetWidth (line 342) | func (m *Model) SetWidth(w int) {
    method Width (line 347) | func (m Model) Width() int {
    method nextFrame (line 351) | func (m *Model) nextFrame() tea.Cmd {
    method barView (line 357) | func (m Model) barView(b *strings.Builder, percent float64, textWidth ...
    method percentageView (line 423) | func (m Model) percentageView(percent float64) string {
    method IsAnimating (line 435) | func (m *Model) IsAnimating() bool {
  function New (line 232) | func New(opts ...Option) Model {

FILE: progress/progress_test.go
  function TestBlend (line 11) | func TestBlend(t *testing.T) {

FILE: spinner/spinner.go
  function nextID (line 16) | func nextID() int {
  type Spinner (line 21) | type Spinner struct
  type Model (line 88) | type Model struct
    method ID (line 105) | func (m Model) ID() int {
    method Update (line 131) | func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
    method View (line 160) | func (m Model) View() string {
    method Tick (line 170) | func (m Model) Tick() tea.Msg {
    method tick (line 184) | func (m Model) tick(id, tag int) tea.Cmd {
  function New (line 110) | func New(opts ...Option) Model {
  type TickMsg (line 124) | type TickMsg struct
  type Option (line 197) | type Option
  function WithSpinner (line 200) | func WithSpinner(spinner Spinner) Option {
  function WithStyle (line 207) | func WithStyle(style lipgloss.Style) Option {

FILE: spinner/spinner_test.go
  function TestSpinnerNew (line 9) | func TestSpinnerNew(t *testing.T) {

FILE: stopwatch/stopwatch.go
  function nextID (line 13) | func nextID() int {
  type Option (line 20) | type Option
  function WithInterval (line 24) | func WithInterval(interval time.Duration) Option {
  type TickMsg (line 31) | type TickMsg struct
  type StartStopMsg (line 44) | type StartStopMsg struct
  type ResetMsg (line 50) | type ResetMsg struct
  type Model (line 55) | type Model struct
    method ID (line 78) | func (m Model) ID() int {
    method Init (line 83) | func (m Model) Init() tea.Cmd {
    method Start (line 88) | func (m Model) Start() tea.Cmd {
    method Stop (line 95) | func (m Model) Stop() tea.Cmd {
    method Toggle (line 102) | func (m Model) Toggle() tea.Cmd {
    method Reset (line 110) | func (m Model) Reset() tea.Cmd {
    method Running (line 117) | func (m Model) Running() bool {
    method Update (line 122) | func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
    method Elapsed (line 155) | func (m Model) Elapsed() time.Duration {
    method View (line 160) | func (m Model) View() string {
  function New (line 66) | func New(opts ...Option) Model {
  function tick (line 164) | func tick(id int, tag int, d time.Duration) tea.Cmd {

FILE: table/table.go
  type Model (line 16) | type Model struct
    method SetStyles (line 122) | func (m *Model) SetStyles(s Styles) {
    method Update (line 202) | func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
    method Focused (line 233) | func (m Model) Focused() bool {
    method Focus (line 239) | func (m *Model) Focus() {
    method Blur (line 245) | func (m *Model) Blur() {
    method View (line 251) | func (m Model) View() string {
    method HelpView (line 258) | func (m Model) HelpView() string {
    method UpdateViewport (line 264) | func (m *Model) UpdateViewport() {
    method SelectedRow (line 287) | func (m Model) SelectedRow() Row {
    method Rows (line 296) | func (m Model) Rows() []Row {
    method Columns (line 301) | func (m Model) Columns() []Column {
    method SetRows (line 306) | func (m *Model) SetRows(r []Row) {
    method SetColumns (line 317) | func (m *Model) SetColumns(c []Column) {
    method SetWidth (line 323) | func (m *Model) SetWidth(w int) {
    method SetHeight (line 329) | func (m *Model) SetHeight(h int) {
    method Height (line 335) | func (m Model) Height() int {
    method Width (line 340) | func (m Model) Width() int {
    method Cursor (line 345) | func (m Model) Cursor() int {
    method SetCursor (line 350) | func (m *Model) SetCursor(n int) {
    method MoveUp (line 357) | func (m *Model) MoveUp(n int) {
    method MoveDown (line 375) | func (m *Model) MoveDown(n int) {
    method GotoTop (line 393) | func (m *Model) GotoTop() {
    method GotoBottom (line 398) | func (m *Model) GotoBottom() {
    method FromValues (line 405) | func (m *Model) FromValues(value, separator string) {
    method headersView (line 418) | func (m Model) headersView() string {
    method renderRow (line 431) | func (m *Model) renderRow(r int) string {
  type Row (line 32) | type Row
  type Column (line 35) | type Column struct
  type KeyMap (line 42) | type KeyMap struct
    method ShortHelp (line 54) | func (km KeyMap) ShortHelp() []key.Binding {
    method FullHelp (line 59) | func (km KeyMap) FullHelp() [][]key.Binding {
  function DefaultKeyMap (line 67) | func DefaultKeyMap() KeyMap {
  type Styles (line 106) | type Styles struct
  function DefaultStyles (line 113) | func DefaultStyles() Styles {
  type Option (line 130) | type Option
  function New (line 133) | func New(opts ...Option) Model {
  function WithColumns (line 153) | func WithColumns(cols []Column) Option {
  function WithRows (line 160) | func WithRows(rows []Row) Option {
  function WithHeight (line 167) | func WithHeight(h int) Option {
  function WithWidth (line 174) | func WithWidth(w int) Option {
  function WithFocused (line 181) | func WithFocused(f bool) Option {
  function WithStyles (line 188) | func WithStyles(s Styles) Option {
  function WithKeyMap (line 195) | func WithKeyMap(km KeyMap) Option {
  function clamp (line 451) | func clamp(v, low, high int) int {

FILE: table/table_test.go
  function TestNew (line 21) | func TestNew(t *testing.T) {
  function TestModel_FromValues (line 210) | func TestModel_FromValues(t *testing.T) {
  function TestModel_FromValues_WithTabSeparator (line 229) | func TestModel_FromValues_WithTabSeparator(t *testing.T) {
  function TestModel_RenderRow (line 247) | func TestModel_RenderRow(t *testing.T) {
  function TestModel_RenderRow_AnsiWidth (line 291) | func TestModel_RenderRow_AnsiWidth(t *testing.T) {
  function TestTableAlignment (line 306) | func TestTableAlignment(t *testing.T) {
  function ansiStrip (line 357) | func ansiStrip(s string) string {
  function TestCursorNavigation (line 363) | func TestCursorNavigation(t *testing.T) {
  function TestModel_SetRows (line 492) | func TestModel_SetRows(t *testing.T) {
  function TestModel_SetColumns (line 511) | func TestModel_SetColumns(t *testing.T) {
  function TestModel_View (line 530) | func TestModel_View(t *testing.T) {
  function TestModel_View_CenteredInABox (line 782) | func TestModel_View_CenteredInABox(t *testing.T) {

FILE: textarea/textarea.go
  constant minHeight (line 29) | minHeight        = 1
  constant defaultHeight (line 30) | defaultHeight    = 6
  constant defaultWidth (line 31) | defaultWidth     = 40
  constant defaultCharLimit (line 32) | defaultCharLimit = 0
  constant defaultMaxHeight (line 33) | defaultMaxHeight = 99
  constant defaultMaxWidth (line 34) | defaultMaxWidth  = 500
  constant maxLines (line 37) | maxLines = 10000
  type pasteMsg (line 42) | type pasteMsg
  type pasteErrMsg (line 43) | type pasteErrMsg struct
  type KeyMap (line 47) | type KeyMap struct
  function DefaultKeyMap (line 78) | func DefaultKeyMap() KeyMap {
  type LineInfo (line 111) | type LineInfo struct
  type PromptInfo (line 142) | type PromptInfo struct
  type CursorStyle (line 148) | type CursorStyle struct
  type Styles (line 178) | type Styles struct
  type StyleState (line 191) | type StyleState struct
    method computedCursorLine (line 202) | func (s StyleState) computedCursorLine() lipgloss.Style {
    method computedCursorLineNumber (line 206) | func (s StyleState) computedCursorLineNumber() lipgloss.Style {
    method computedEndOfBuffer (line 213) | func (s StyleState) computedEndOfBuffer() lipgloss.Style {
    method computedLineNumber (line 217) | func (s StyleState) computedLineNumber() lipgloss.Style {
    method computedPlaceholder (line 221) | func (s StyleState) computedPlaceholder() lipgloss.Style {
    method computedPrompt (line 225) | func (s StyleState) computedPrompt() lipgloss.Style {
    method computedText (line 229) | func (s StyleState) computedText() lipgloss.Style {
  type line (line 235) | type line struct
    method Hash (line 241) | func (w line) Hash() string {
  type Model (line 247) | type Model struct
    method Styles (line 420) | func (m Model) Styles() Styles {
    method SetStyles (line 425) | func (m *Model) SetStyles(s Styles) {
    method VirtualCursor (line 431) | func (m Model) VirtualCursor() bool {
    method SetVirtualCursor (line 436) | func (m *Model) SetVirtualCursor(v bool) {
    method updateVirtualCursorStyle (line 443) | func (m *Model) updateVirtualCursorStyle() {
    method SetValue (line 464) | func (m *Model) SetValue(s string) {
    method InsertString (line 470) | func (m *Model) InsertString(s string) {
    method InsertRune (line 475) | func (m *Model) InsertRune(r rune) {
    method insertRunesFromUserInput (line 480) | func (m *Model) insertRunesFromUserInput(runes []rune) {
    method Value (line 569) | func (m Model) Value() string {
    method Length (line 584) | func (m *Model) Length() int {
    method LineCount (line 594) | func (m *Model) LineCount() int {
    method Line (line 599) | func (m Model) Line() int {
    method Column (line 604) | func (m Model) Column() int {
    method ScrollYOffset (line 610) | func (m Model) ScrollYOffset() int {
    method ScrollPercent (line 616) | func (m Model) ScrollPercent() float64 {
    method setCursorLineRelative (line 622) | func (m *Model) setCursorLineRelative(delta int) {
    method CursorDown (line 680) | func (m *Model) CursorDown() {
    method CursorUp (line 685) | func (m *Model) CursorUp() {
    method SetCursorColumn (line 691) | func (m *Model) SetCursorColumn(col int) {
    method CursorStart (line 699) | func (m *Model) CursorStart() {
    method CursorEnd (line 704) | func (m *Model) CursorEnd() {
    method Focused (line 709) | func (m Model) Focused() bool {
    method activeStyle (line 715) | func (m Model) activeStyle() *StyleState {
    method Focus (line 724) | func (m *Model) Focus() tea.Cmd {
    method Blur (line 731) | func (m *Model) Blur() {
    method Reset (line 737) | func (m *Model) Reset() {
    method Word (line 747) | func (m *Model) Word() string {
    method san (line 781) | func (m *Model) san() runeutil.Sanitizer {
    method deleteBeforeCursor (line 792) | func (m *Model) deleteBeforeCursor() {
    method deleteAfterCursor (line 800) | func (m *Model) deleteAfterCursor() {
    method transposeLeft (line 809) | func (m *Model) transposeLeft() {
    method deleteWordLeft (line 824) | func (m *Model) deleteWordLeft() {
    method deleteWordRight (line 863) | func (m *Model) deleteWordRight() {
    method characterRight (line 893) | func (m *Model) characterRight() {
    method characterLeft (line 907) | func (m *Model) characterLeft(insideLine bool) {
    method wordLeft (line 923) | func (m *Model) wordLeft() {
    method wordRight (line 942) | func (m *Model) wordRight() {
    method doWordRight (line 946) | func (m *Model) doWordRight(fn func(charIdx int, pos int)) {
    method uppercaseRight (line 968) | func (m *Model) uppercaseRight() {
    method lowercaseRight (line 975) | func (m *Model) lowercaseRight() {
    method capitalizeRight (line 982) | func (m *Model) capitalizeRight() {
    method LineInfo (line 992) | func (m Model) LineInfo() LineInfo {
    method repositionView (line 1033) | func (m *Model) repositionView() {
    method Width (line 1044) | func (m Model) Width() int {
    method MoveToBegin (line 1049) | func (m *Model) MoveToBegin() {
    method MoveToEnd (line 1056) | func (m *Model) MoveToEnd() {
    method PageUp (line 1064) | func (m *Model) PageUp() {
    method PageDown (line 1077) | func (m *Model) PageDown() {
    method SetWidth (line 1095) | func (m *Model) SetWidth(w int) {
    method SetPromptFunc (line 1145) | func (m *Model) SetPromptFunc(promptWidth int, fn func(PromptInfo) str...
    method Height (line 1151) | func (m Model) Height() int {
    method SetHeight (line 1156) | func (m *Model) SetHeight(h int) {
    method Update (line 1169) | func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
    method view (line 1317) | func (m *Model) view() string {
    method View (line 1415) | func (m Model) View() string {
    method promptView (line 1427) | func (m Model) promptView(displayLine int) (prompt string) {
    method lineNumberView (line 1451) | func (m Model) lineNumberView(n int, isCursorLine bool) (str string) {
    method placeholderView (line 1478) | func (m Model) placeholderView() string {
    method Cursor (line 1578) | func (m Model) Cursor() *tea.Cursor {
    method memoizedWrap (line 1607) | func (m Model) memoizedWrap(runes []rune, width int) [][]rune {
    method cursorLineNumber (line 1619) | func (m Model) cursorLineNumber() int {
    method mergeLineBelow (line 1631) | func (m *Model) mergeLineBelow(row int) {
    method mergeLineAbove (line 1651) | func (m *Model) mergeLineAbove(row int) {
    method splitLine (line 1673) | func (m *Model) splitLine(row, col int) {
  function New (line 341) | func New() Model {
  function DefaultStyles (line 377) | func DefaultStyles(isDark bool) Styles {
  function DefaultLightStyles (line 410) | func DefaultLightStyles() Styles {
  function DefaultDarkStyles (line 415) | func DefaultDarkStyles() Styles {
  function Blink (line 1561) | func Blink() tea.Msg {
  function Paste (line 1691) | func Paste() tea.Msg {
  function wrap (line 1699) | func wrap(runes []rune, width int) [][]rune {
  function repeatSpaces (line 1764) | func repeatSpaces(n int) []rune {
  function numDigits (line 1769) | func numDigits(n int) int {
  function clamp (line 1782) | func clamp(v, low, high int) int {
  function abs (line 1789) | func abs(n int) int {

FILE: textarea/textarea_test.go
  function TestVerticalScrolling (line 15) | func TestVerticalScrolling(t *testing.T) {
  function TestWordWrapOverflowing (line 58) | func TestWordWrapOverflowing(t *testing.T) {
  function TestValueSoftWrap (line 100) | func TestValueSoftWrap(t *testing.T) {
  function TestSetValue (line 123) | func TestSetValue(t *testing.T) {
  function TestInsertString (line 146) | func TestInsertString(t *testing.T) {
  function TestCanHandleEmoji (line 168) | func TestCanHandleEmoji(t *testing.T) {
  function TestVerticalNavigationKeepsCursorHorizontalPosition (line 203) | func TestVerticalNavigationKeepsCursorHorizontalPosition(t *testing.T) {
  function TestVerticalNavigationShouldRememberPositionWhileTraversing (line 251) | func TestVerticalNavigationShouldRememberPositionWhileTraversing(t *test...
  function TestView (line 331) | func TestView(t *testing.T) {
  function TestWord (line 1921) | func TestWord(t *testing.T) {
  function newTextArea (line 1977) | func newTextArea() Model {
  function keyPress (line 1990) | func keyPress(key rune) tea.Msg {
  function sendString (line 1994) | func sendString(m Model, str string) Model {
  function stripString (line 2002) | func stripString(str string) string {

FILE: textinput/styles.go
  function DefaultStyles (line 13) | func DefaultStyles(isDark bool) Styles {
  function DefaultLightStyles (line 38) | func DefaultLightStyles() Styles {
  function DefaultDarkStyles (line 43) | func DefaultDarkStyles() Styles {
  type Styles (line 50) | type Styles struct
  type StyleState (line 63) | type StyleState struct
  type CursorStyle (line 71) | type CursorStyle struct

FILE: textinput/textinput.go
  type pasteMsg (line 23) | type pasteMsg
  type pasteErrMsg (line 24) | type pasteErrMsg struct
  type EchoMode (line 28) | type EchoMode
  constant EchoNormal (line 32) | EchoNormal EchoMode = iota
  constant EchoPassword (line 36) | EchoPassword
  constant EchoNone (line 40) | EchoNone
  type ValidateFunc (line 44) | type ValidateFunc
  type KeyMap (line 47) | type KeyMap struct
  function DefaultKeyMap (line 68) | func DefaultKeyMap() KeyMap {
  type Model (line 90) | type Model struct
    method VirtualCursor (line 177) | func (m Model) VirtualCursor() bool {
    method SetVirtualCursor (line 183) | func (m *Model) SetVirtualCursor(v bool) {
    method Styles (line 189) | func (m Model) Styles() Styles {
    method SetStyles (line 194) | func (m *Model) SetStyles(s Styles) {
    method Width (line 200) | func (m Model) Width() int {
    method SetWidth (line 205) | func (m *Model) SetWidth(w int) {
    method SetValue (line 210) | func (m *Model) SetValue(s string) {
    method setValueInternal (line 218) | func (m *Model) setValueInternal(runes []rune, err error) {
    method Value (line 235) | func (m Model) Value() string {
    method Position (line 240) | func (m Model) Position() int {
    method SetCursor (line 246) | func (m *Model) SetCursor(pos int) {
    method CursorStart (line 252) | func (m *Model) CursorStart() {
    method CursorEnd (line 257) | func (m *Model) CursorEnd() {
    method Focused (line 262) | func (m Model) Focused() bool {
    method Focus (line 268) | func (m *Model) Focus() tea.Cmd {
    method Blur (line 275) | func (m *Model) Blur() {
    method Reset (line 281) | func (m *Model) Reset() {
    method SetSuggestions (line 287) | func (m *Model) SetSuggestions(suggestions []string) {
    method san (line 297) | func (m *Model) san() runeutil.Sanitizer {
    method insertRunesFromUserInput (line 307) | func (m *Model) insertRunesFromUserInput(v []rune) {
    method handleOverflow (line 355) | func (m *Model) handleOverflow() {
    method deleteBeforeCursor (line 399) | func (m *Model) deleteBeforeCursor() {
    method deleteAfterCursor (line 409) | func (m *Model) deleteAfterCursor() {
    method deleteWordBackward (line 416) | func (m *Model) deleteWordBackward() {
    method deleteWordForward (line 463) | func (m *Model) deleteWordForward() {
    method wordBackward (line 504) | func (m *Model) wordBackward() {
    method wordForward (line 536) | func (m *Model) wordForward() {
    method echoTransform (line 566) | func (m Model) echoTransform(v string) string {
    method Update (line 580) | func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
    method View (line 684) | func (m Model) View() string {
    method promptView (line 736) | func (m Model) promptView() string {
    method placeholderView (line 741) | func (m Model) placeholderView() string {
    method completionView (line 803) | func (m Model) completionView(offset int) string {
    method getSuggestions (line 816) | func (m *Model) getSuggestions(sugs [][]rune) []string {
    method AvailableSuggestions (line 825) | func (m *Model) AvailableSuggestions() []string {
    method MatchedSuggestions (line 830) | func (m *Model) MatchedSuggestions() []string {
    method CurrentSuggestionIndex (line 835) | func (m *Model) CurrentSuggestionIndex() int {
    method CurrentSuggestion (line 840) | func (m *Model) CurrentSuggestion() string {
    method canAcceptSuggestion (line 850) | func (m *Model) canAcceptSuggestion() bool {
    method updateSuggestions (line 855) | func (m *Model) updateSuggestions() {
    method nextSuggestion (line 881) | func (m *Model) nextSuggestion() {
    method previousSuggestion (line 889) | func (m *Model) previousSuggestion() {
    method validate (line 896) | func (m Model) validate(v []rune) error {
    method Cursor (line 916) | func (m Model) Cursor() *tea.Cursor {
    method updateVirtualCursorStyle (line 940) | func (m *Model) updateVirtualCursorStyle() {
    method activeStyle (line 963) | func (m Model) activeStyle() *StyleState {
  function New (line 157) | func New() Model {
  function Blink (line 783) | func Blink() tea.Msg {
  function Paste (line 788) | func Paste() tea.Msg {
  function clamp (line 796) | func clamp(v, low, high int) int {

FILE: textinput/textinput_test.go
  function Test_CurrentSuggestion (line 12) | func Test_CurrentSuggestion(t *testing.T) {
  function Test_SlicingOutsideCap (line 44) | func Test_SlicingOutsideCap(t *testing.T) {
  function TestChinesePlaceholder (line 51) | func TestChinesePlaceholder(t *testing.T) {
  function TestPlaceholderTruncate (line 64) | func TestPlaceholderTruncate(t *testing.T) {
  function ExampleValidateFunc (line 77) | func ExampleValidateFunc() {
  function keyPress (line 110) | func keyPress(key rune) tea.Msg {
  function sendString (line 114) | func sendString(m Model, str string) Model {

FILE: timer/timer.go
  function nextID (line 13) | func nextID() int {
  type Option (line 20) | type Option
  function WithInterval (line 24) | func WithInterval(interval time.Duration) Option {
  type StartStopMsg (line 58) | type StartStopMsg struct
  type TickMsg (line 64) | type TickMsg struct
  type TimeoutMsg (line 85) | type TimeoutMsg struct
  type Model (line 90) | type Model struct
    method ID (line 118) | func (m Model) ID() int {
    method Running (line 124) | func (m Model) Running() bool {
    method Timedout (line 132) | func (m Model) Timedout() bool {
    method Init (line 137) | func (m Model) Init() tea.Cmd {
    method Update (line 142) | func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
    method View (line 170) | func (m Model) View() string {
    method Start (line 175) | func (m *Model) Start() tea.Cmd {
    method Stop (line 180) | func (m *Model) Stop() tea.Cmd {
    method Toggle (line 185) | func (m *Model) Toggle() tea.Cmd {
    method tick (line 189) | func (m Model) tick() tea.Cmd {
    method timedout (line 195) | func (m Model) timedout() tea.Cmd {
    method startStop (line 204) | func (m Model) startStop(v bool) tea.Cmd {
  function New (line 103) | func New(timeout time.Duration, opts ...Option) Model {

FILE: viewport/highlight.go
  function parseMatches (line 20) | func parseMatches(
  type highlightInfo (line 104) | type highlightInfo struct
    method coords (line 113) | func (hi highlightInfo) coords() (int, int, int) {
  function makeHighlightRanges (line 124) | func makeHighlightRanges(

FILE: viewport/keymap.go
  type KeyMap (line 11) | type KeyMap struct
  function DefaultKeyMap (line 23) | func DefaultKeyMap() KeyMap {

FILE: viewport/viewport.go
  constant defaultHorizontalStep (line 16) | defaultHorizontalStep = 6
  type Option (line 23) | type Option
  function WithWidth (line 27) | func WithWidth(w int) Option {
  function WithHeight (line 35) | func WithHeight(h int) Option {
  function New (line 43) | func New(opts ...Option) (m Model) {
  type Model (line 52) | type Model struct
    method setInitialValues (line 148) | func (m *Model) setInitialValues() {
    method Init (line 158) | func (m Model) Init() tea.Cmd {
    method Height (line 163) | func (m Model) Height() int {
    method SetHeight (line 168) | func (m *Model) SetHeight(h int) {
    method Width (line 173) | func (m Model) Width() int {
    method SetWidth (line 178) | func (m *Model) SetWidth(w int) {
    method AtTop (line 183) | func (m Model) AtTop() bool {
    method AtBottom (line 189) | func (m Model) AtBottom() bool {
    method PastBottom (line 195) | func (m Model) PastBottom() bool {
    method ScrollPercent (line 200) | func (m Model) ScrollPercent() float64 {
    method HorizontalScrollPercent (line 214) | func (m Model) HorizontalScrollPercent() float64 {
    method SetContent (line 226) | func (m *Model) SetContent(s string) {
    method SetContentLines (line 233) | func (m *Model) SetContentLines(lines []string) {
    method GetContent (line 266) | func (m Model) GetContent() string {
    method calculateLine (line 273) | func (m Model) calculateLine(yoffset int) (total, ridx, voffset int) {
    method maxYOffset (line 303) | func (m Model) maxYOffset() int {
    method maxXOffset (line 310) | func (m Model) maxXOffset() int {
    method maxWidth (line 316) | func (m Model) maxWidth() int {
    method maxHeight (line 326) | func (m Model) maxHeight() int {
    method visibleLines (line 332) | func (m Model) visibleLines() (lines []string) {
    method styleLines (line 368) | func (m Model) styleLines(lines []string, offset int) []string {
    method highlightLines (line 380) | func (m Model) highlightLines(lines []string, offset int) []string {
    method softWrap (line 406) | func (m Model) softWrap(lines []string, maxWidth, maxHeight, total, ri...
    method setupGutter (line 448) | func (m Model) setupGutter(lines []string, total, ridx int) []string {
    method SetYOffset (line 464) | func (m *Model) SetYOffset(n int) {
    method YOffset (line 469) | func (m *Model) YOffset() int { return m.yOffset }
    method EnsureVisible (line 472) | func (m *Model) EnsureVisible(line, colstart, colend int) {
    method PageDown (line 486) | func (m *Model) PageDown() {
    method PageUp (line 494) | func (m *Model) PageUp() {
    method HalfPageDown (line 502) | func (m *Model) HalfPageDown() {
    method HalfPageUp (line 510) | func (m *Model) HalfPageUp() {
    method ScrollDown (line 518) | func (m *Model) ScrollDown(n int) {
    method ScrollUp (line 530) | func (m *Model) ScrollUp(n int) {
    method SetHorizontalStep (line 543) | func (m *Model) SetHorizontalStep(n int) {
    method XOffset (line 548) | func (m *Model) XOffset() int { return m.xOffset }
    method SetXOffset (line 552) | func (m *Model) SetXOffset(n int) {
    method ScrollLeft (line 560) | func (m *Model) ScrollLeft(n int) {
    method ScrollRight (line 565) | func (m *Model) ScrollRight(n int) {
    method TotalLineCount (line 570) | func (m Model) TotalLineCount() int {
    method VisibleLineCount (line 576) | func (m Model) VisibleLineCount() int {
    method GotoTop (line 581) | func (m *Model) GotoTop() (lines []string) {
    method GotoBottom (line 591) | func (m *Model) GotoBottom() (lines []string) {
    method SetHighlights (line 605) | func (m *Model) SetHighlights(matches [][]int) {
    method ClearHighlights (line 615) | func (m *Model) ClearHighlights() {
    method showHighlight (line 620) | func (m *Model) showHighlight() {
    method HighlightNext (line 629) | func (m *Model) HighlightNext() {
    method HighlightPrevious (line 638) | func (m *Model) HighlightPrevious() {
    method findNearestMatch (line 646) | func (m Model) findNearestMatch() int {
    method Update (line 656) | func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
    method updateAsModel (line 663) | func (m Model) updateAsModel(msg tea.Msg) Model {
    method View (line 728) | func (m Model) View() string {
  type GutterFunc (line 131) | type GutterFunc
  type GutterContext (line 137) | type GutterContext struct
  function clamp (line 752) | func clamp[T cmp.Ordered](v, low, high T) T {
  function maxLineWidth (line 759) | func maxLineWidth(lines []string) int {

FILE: viewport/viewport_test.go
  type suffixedTest (line 15) | type suffixedTest struct
    method Name (line 20) | func (s *suffixedTest) Name() string {
  function withSuffix (line 27) | func withSuffix(t testing.TB, suffix string) testing.TB {
  constant textContentList (line 33) | textContentList = `57 Precepts of narcissistic comedy character Zote fro...
  function TestNew (line 51) | func TestNew(t *testing.T) {
  function TestSetInitialValues (line 77) | func TestSetInitialValues(t *testing.T) {
  function TestSetHorizontalStep (line 92) | func TestSetHorizontalStep(t *testing.T) {
  function TestMoveLeft (line 128) | func TestMoveLeft(t *testing.T) {
  function TestMoveRight (line 164) | func TestMoveRight(t *testing.T) {
  function TestResetIndent (line 186) | func TestResetIndent(t *testing.T) {
  function TestVisibleLines (line 204) | func TestVisibleLines(t *testing.T) {
  function TestRightOverscroll (line 389) | func TestRightOverscroll(t *testing.T) {
  function TestMatchesToHighlights (line 411) | func TestMatchesToHighlights(t *testing.T) {
  function testHighlights (line 560) | func testHighlights(tb testing.TB, content string, re *regexp.Regexp, ex...
  function TestSizing (line 588) | func TestSizing(t *testing.T) {
  function BenchmarkView (line 737) | func BenchmarkView(b *testing.B) {
Condensed preview — 98 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (446K chars).
[
  {
    "path": ".github/CODEOWNERS",
    "chars": 323,
    "preview": "*  @meowgorithm @bashbunni\ncursor/  @aymanbagabas\nfilepicker/  @bashbunni\nhelp/  @meowgorithm\nkey/  @meowgorithm\nlist/  "
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "chars": 876,
    "preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Describe the b"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "chars": 120,
    "preview": "blank_issues_enabled: true\ncontact_links:\n- name: Discord\n  url: https://charm.sh/discord\n  about: Chat on our Discord.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "chars": 604,
    "preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: enhancement\nassignees: ''\n\n---\n\n**Is"
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 996,
    "preview": "version: 2\n\nupdates:\n  - package-ecosystem: \"gomod\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n      day:"
  },
  {
    "path": ".github/workflows/build.yml",
    "chars": 1149,
    "preview": "name: build\n\non: [push, pull_request]\n\njobs:\n  test:\n    strategy:\n      matrix:\n        go-version: [stable, oldstable]"
  },
  {
    "path": ".github/workflows/coverage.yml",
    "chars": 698,
    "preview": "name: coverage\non: [push, pull_request]\n\njobs:\n  coverage:\n    strategy:\n      matrix:\n        go-version: [^1.22]\n     "
  },
  {
    "path": ".github/workflows/dependabot-sync.yml",
    "chars": 419,
    "preview": "name: dependabot-sync\non:\n  schedule:\n    - cron: \"0 0 * * 0\" # every Sunday at midnight\n  workflow_dispatch: # allows m"
  },
  {
    "path": ".github/workflows/lint-sync.yml",
    "chars": 271,
    "preview": "name: lint-sync\non:\n  schedule:\n    # every Sunday at midnight\n    - cron: \"0 0 * * 0\"\n  workflow_dispatch: # allows man"
  },
  {
    "path": ".github/workflows/lint.yml",
    "chars": 208,
    "preview": "name: lint\non:\n  push:\n  pull_request:\n\njobs:\n  lint:\n    uses: charmbracelet/meta/.github/workflows/lint.yml@main\n    w"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 1106,
    "preview": "name: goreleaser\n\non:\n  push:\n    tags:\n      - v*.*.*\n\nconcurrency:\n  group: goreleaser\n  cancel-in-progress: true\n\njob"
  },
  {
    "path": ".gitignore",
    "chars": 16,
    "preview": ".DS_Store\ndist/\n"
  },
  {
    "path": ".golangci.yml",
    "chars": 703,
    "preview": "version: \"2\"\nrun:\n  tests: false\nlinters:\n  enable:\n    - bodyclose\n    - exhaustive\n    - goconst\n    - godot\n    - god"
  },
  {
    "path": ".goreleaser.yml",
    "chars": 168,
    "preview": "# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json\nversion: 2\nincludes:\n  - from_url:\n      u"
  },
  {
    "path": "LICENSE",
    "chars": 1081,
    "preview": "MIT License\n\nCopyright (c) 2020-2026 Charmbracelet, Inc.\n\nPermission is hereby granted, free of charge, to any person ob"
  },
  {
    "path": "README.md",
    "chars": 8856,
    "preview": "# Bubbles\n\n<img src=\"https://github.com/user-attachments/assets/b89fa46e-d451-4b33-a009-c68d4765520f\" width=\"350\" />\n\n[!"
  },
  {
    "path": "Taskfile.yaml",
    "chars": 183,
    "preview": "# https://taskfile.dev\n\nversion: '3'\n\ntasks:\n  lint:\n    desc: Run lint\n    cmds:\n      - golangci-lint run\n\n  test:\n   "
  },
  {
    "path": "UPGRADE_GUIDE_V2.md",
    "chars": 17853,
    "preview": "# Upgrading to Bubbles v2\n\nThis guide covers every breaking change when migrating from Bubbles v1 (`github.com/charmbrac"
  },
  {
    "path": "bubbles.go",
    "chars": 179,
    "preview": "// Package bubbles provides some components for Bubble Tea applications. These\n// components are used in production in G"
  },
  {
    "path": "cursor/cursor.go",
    "chars": 5251,
    "preview": "// Package cursor provides a virtual cursor to support the textinput and\n// textarea elements.\npackage cursor\n\nimport (\n"
  },
  {
    "path": "cursor/cursor_test.go",
    "chars": 1434,
    "preview": "package cursor\n\nimport (\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n)\n\n// TestBlinkCmdDataRace tests for a race on [Cursor.blinkTag].\n//"
  },
  {
    "path": "filepicker/filepicker.go",
    "chars": 13467,
    "preview": "// Package filepicker provides a file picker component for Bubble Tea\n// applications.\npackage filepicker\n\nimport (\n\t\"fm"
  },
  {
    "path": "filepicker/hidden_unix.go",
    "chars": 218,
    "preview": "//go:build !windows\n// +build !windows\n\npackage filepicker\n\nimport \"strings\"\n\n// IsHidden reports whether a file is hidd"
  },
  {
    "path": "filepicker/hidden_windows.go",
    "chars": 461,
    "preview": "//go:build windows\n// +build windows\n\npackage filepicker\n\nimport (\n\t\"syscall\"\n)\n\n// IsHidden reports whether a file is h"
  },
  {
    "path": "go.mod",
    "chars": 1275,
    "preview": "module charm.land/bubbles/v2\n\ngo 1.25.0\n\nrequire (\n\tcharm.land/bubbletea/v2 v2.0.2\n\tcharm.land/lipgloss/v2 v2.0.2\n\tgithu"
  },
  {
    "path": "go.sum",
    "chars": 4814,
    "preview": "charm.land/bubbletea/v2 v2.0.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0=\ncharm.land/bubbletea/v2 v2.0.2/go.mod h1:"
  },
  {
    "path": "help/help.go",
    "chars": 6393,
    "preview": "// Package help provides a simple help view for Bubble Tea applications.\npackage help\n\nimport (\n\t\"strings\"\n\n\t\"charm.land"
  },
  {
    "path": "help/help_test.go",
    "chars": 831,
    "preview": "package help\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"charm.land/bubbles/v2/key\"\n\t\"github.com/charmbracelet/x/ansi\"\n\t\"github.com/c"
  },
  {
    "path": "help/testdata/TestFullHelp/full_help_20_width.golden",
    "chars": 16,
    "preview": "enter continue …"
  },
  {
    "path": "help/testdata/TestFullHelp/full_help_30_width.golden",
    "chars": 55,
    "preview": "enter continue | esc back …\n                 ?   help  "
  },
  {
    "path": "help/testdata/TestFullHelp/full_help_40_width.golden",
    "chars": 119,
    "preview": "enter continue | esc back | H      home\n                 ?   help   ctrl+c quit\n                            ctrl+l log "
  },
  {
    "path": "internal/memoization/memoization.go",
    "chars": 3852,
    "preview": "// Package memoization implement a simple memoization cache. It's designed to\n// improve performance in textarea.\npackag"
  },
  {
    "path": "internal/memoization/memoization_test.go",
    "chars": 7446,
    "preview": "package memoization\n\nimport (\n\t\"encoding/binary\"\n\t\"fmt\"\n\t\"os\"\n\t\"slices\"\n\t\"testing\"\n)\n\ntype actionType int\n\nconst (\n\tset "
  },
  {
    "path": "internal/runeutil/runeutil.go",
    "chars": 2713,
    "preview": "// Package runeutil provides utility functions for tidying up incoming runes\n// from Key messages.\npackage runeutil\n\nimp"
  },
  {
    "path": "internal/runeutil/runeutil_test.go",
    "chars": 938,
    "preview": "package runeutil\n\nimport (\n\t\"testing\"\n\t\"unicode/utf8\"\n)\n\nfunc TestSanitize(t *testing.T) {\n\ttd := []struct {\n\t\tinput, ou"
  },
  {
    "path": "key/key.go",
    "chars": 3593,
    "preview": "// Package key provides some types and functions for generating user-definable\n// keymappings useful in Bubble Tea compo"
  },
  {
    "path": "key/key_test.go",
    "chars": 445,
    "preview": "package key\n\nimport (\n\t\"testing\"\n)\n\nfunc TestBinding_Enabled(t *testing.T) {\n\tbinding := NewBinding(\n\t\tWithKeys(\"k\", \"up"
  },
  {
    "path": "list/README.md",
    "chars": 2331,
    "preview": "# Frequently Asked Questions\n\nThese are some of the most commonly asked questions regarding the `list` bubble.\n\n## Addin"
  },
  {
    "path": "list/defaultitem.go",
    "chars": 6499,
    "preview": "package list\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n\n\t\"charm.land/bubbles/v2/key\"\n\ttea \"charm.land/bubbletea/v2\"\n\t\"charm.land"
  },
  {
    "path": "list/keys.go",
    "chars": 2405,
    "preview": "package list\n\nimport \"charm.land/bubbles/v2/key\"\n\n// KeyMap defines keybindings. It satisfies to the help.KeyMap interfa"
  },
  {
    "path": "list/list.go",
    "chars": 33769,
    "preview": "// Package list provides a feature-rich Bubble Tea component for browsing\n// a general purpose list of items. It feature"
  },
  {
    "path": "list/list_test.go",
    "chars": 3731,
    "preview": "package list\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"reflect\"\n\t\"slices\"\n\t\"strings\"\n\t\"testing\"\n\n\ttea \"charm.land/bubbletea/v2\"\n)\n\ntype i"
  },
  {
    "path": "list/style.go",
    "chars": 3064,
    "preview": "package list\n\nimport (\n\t\"charm.land/bubbles/v2/textinput\"\n\t\"charm.land/lipgloss/v2\"\n)\n\nconst (\n\tbullet   = \"•\"\n\tellipsis"
  },
  {
    "path": "paginator/paginator.go",
    "chars": 4976,
    "preview": "// Package paginator provides a Bubble Tea package for calculating pagination\n// and rendering pagination info. Note tha"
  },
  {
    "path": "paginator/paginator_test.go",
    "chars": 4183,
    "preview": "package paginator\n\nimport (\n\t\"testing\"\n\n\ttea \"charm.land/bubbletea/v2\"\n)\n\nfunc TestNew(t *testing.T) {\n\tmodel := New()\n\n"
  },
  {
    "path": "progress/progress.go",
    "chars": 12359,
    "preview": "// Package progress provides a simple progress bar for Bubble Tea applications.\npackage progress\n\nimport (\n\t\"fmt\"\n\t\"imag"
  },
  {
    "path": "progress/progress_test.go",
    "chars": 1987,
    "preview": "package progress\n\nimport (\n\t\"image/color\"\n\t\"testing\"\n\n\t\"charm.land/lipgloss/v2\"\n\t\"github.com/charmbracelet/x/exp/golden\""
  },
  {
    "path": "progress/testdata/TestBlend/10w-red-to-green-50perc-full-block.golden",
    "chars": 126,
    "preview": "\u001b[38;2;255;0;0m█\u001b[m\u001b[38;2;246;78;0m█\u001b[m\u001b[38;2;236;111;0m█\u001b[m\u001b[38;2;223;138;0m█\u001b[m\u001b[38;2;209;161;0m█\u001b[m\u001b[38;2;96;96;96m░░"
  },
  {
    "path": "progress/testdata/TestBlend/10w-red-to-green-50perc.golden",
    "chars": 199,
    "preview": "\u001b[38;2;255;0;0;48;2;251;52;0m▌\u001b[m\u001b[38;2;247;76;0;48;2;242;93;0m▌\u001b[m\u001b[38;2;237;108;0;48;2;231;122;0m▌\u001b[m\u001b[38;2;225;134;0;"
  },
  {
    "path": "progress/testdata/TestBlend/10w-red-to-green-scaled-50perc.golden",
    "chars": 199,
    "preview": "\u001b[38;2;255;0;0;48;2;246;78;0m▌\u001b[m\u001b[38;2;236;111;0;48;2;223;138;0m▌\u001b[m\u001b[38;2;209;161;0;48;2;192;181;0m▌\u001b[m\u001b[38;2;171;201;"
  },
  {
    "path": "progress/testdata/TestBlend/30w-colorfunc-rgb-100perc.golden",
    "chars": 979,
    "preview": "\u001b[38;2;255;0;0;48;2;255;0;0m▌\u001b[m\u001b[38;2;255;0;0;48;2;255;0;0m▌\u001b[m\u001b[38;2;255;0;0;48;2;255;0;0m▌\u001b[m\u001b[38;2;255;0;0;48;2;255;"
  },
  {
    "path": "progress/testdata/TestBlend/30w-red-to-green-100perc.golden",
    "chars": 1080,
    "preview": "\u001b[38;2;255;0;0;48;2;254;27;0m▌\u001b[m\u001b[38;2;253;41;0;48;2;251;51;0m▌\u001b[m\u001b[38;2;250;60;0;48;2;248;68;0m▌\u001b[m\u001b[38;2;247;74;0;48;"
  },
  {
    "path": "progress/testdata/TestBlend/30w-red-to-green-scaled-100perc.golden",
    "chars": 1080,
    "preview": "\u001b[38;2;255;0;0;48;2;254;27;0m▌\u001b[m\u001b[38;2;253;41;0;48;2;251;51;0m▌\u001b[m\u001b[38;2;250;60;0;48;2;248;68;0m▌\u001b[m\u001b[38;2;247;74;0;48;"
  },
  {
    "path": "spinner/spinner.go",
    "chars": 4835,
    "preview": "// Package spinner provides a spinner component for Bubble Tea applications.\npackage spinner\n\nimport (\n\t\"sync/atomic\"\n\t\""
  },
  {
    "path": "spinner/spinner_test.go",
    "chars": 1357,
    "preview": "package spinner_test\n\nimport (\n\t\"testing\"\n\n\t\"charm.land/bubbles/v2/spinner\"\n)\n\nfunc TestSpinnerNew(t *testing.T) {\n\tasse"
  },
  {
    "path": "stopwatch/stopwatch.go",
    "chars": 3570,
    "preview": "// Package stopwatch provides a simple stopwatch component.\npackage stopwatch\n\nimport (\n\t\"sync/atomic\"\n\t\"time\"\n\n\ttea \"ch"
  },
  {
    "path": "table/table.go",
    "chars": 10874,
    "preview": "// Package table provides a simple table component for Bubble Tea applications.\npackage table\n\nimport (\n\t\"strings\"\n\n\t\"ch"
  },
  {
    "path": "table/table_test.go",
    "chars": 17542,
    "preview": "package table\n\nimport (\n\t\"reflect\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"charm.land/bubbles/v2/help\"\n\t\"charm.land/bubbles/v2/viewport"
  },
  {
    "path": "table/testdata/TestModel_View/Bordered_cells.golden",
    "chars": 1253,
    "preview": "Name                     Country of Orig…Dunk-able   \n┌─────────────────────────┐┌────────────────┐┌────────────┐\n│Choco"
  },
  {
    "path": "table/testdata/TestModel_View/Bordered_headers.golden",
    "chars": 1499,
    "preview": "┌─────────────────────────┐┌────────────────┐┌────────────┐\n│Name                     ││Country of Orig…││Dunk-able   │\n"
  },
  {
    "path": "table/testdata/TestModel_View/Empty.golden",
    "chars": 1220,
    "preview": "\n                                                            \n                                                          "
  },
  {
    "path": "table/testdata/TestModel_View/Extra_padding.golden",
    "chars": 878,
    "preview": "                                                                 \n                                                      "
  },
  {
    "path": "table/testdata/TestModel_View/Height_greater_than_rows.golden",
    "chars": 359,
    "preview": " Name                       Country of Orig…  Dunk-able    \n Chocolate Digestives       UK                Yes          \n"
  },
  {
    "path": "table/testdata/TestModel_View/Height_less_than_rows.golden",
    "chars": 119,
    "preview": " Name                       Country of Orig…  Dunk-able    \n Chocolate Digestives       UK                Yes          "
  },
  {
    "path": "table/testdata/TestModel_View/Modified_viewport_height.golden",
    "chars": 179,
    "preview": " Name                       Country of Orig…  Dunk-able    \n Chocolate Digestives       UK                Yes          \n"
  },
  {
    "path": "table/testdata/TestModel_View/Multiple_rows_and_columns.golden",
    "chars": 1259,
    "preview": " Name                       Country of Orig…  Dunk-able    \n Chocolate Digestives       UK                Yes          \n"
  },
  {
    "path": "table/testdata/TestModel_View/No_padding.golden",
    "chars": 539,
    "preview": "Name                     Country of Orig…Dunk-able   \nChocolate Digestives     UK              Yes         \nTim Tams    "
  },
  {
    "path": "table/testdata/TestModel_View/Single_row_and_column.golden",
    "chars": 587,
    "preview": " Name                      \n Chocolate Digestives      \n                           \n                           \n        "
  },
  {
    "path": "table/testdata/TestModel_View/Width_greater_than_columns.golden",
    "chars": 1679,
    "preview": " Name                       Country of Orig…  Dunk-able    \n Chocolate Digestives       UK                Yes           "
  },
  {
    "path": "table/testdata/TestModel_View/Width_less_than_columns.golden",
    "chars": 645,
    "preview": " Name                     Country of Origin    Dunk-able\n Chocolate Digestives     UK                   Yes\n Tim Tams   "
  },
  {
    "path": "table/testdata/TestModel_View_CenteredInABox.golden",
    "chars": 663,
    "preview": "┌────────────────────────────────────────────────────────────────────────────────┐\n│           Name                     "
  },
  {
    "path": "table/testdata/TestTableAlignment/No_border.golden",
    "chars": 299,
    "preview": " Name                       Country of Orig…  Dunk-able    \n Chocolate Digestives       UK                Yes          \n"
  },
  {
    "path": "table/testdata/TestTableAlignment/With_border.golden",
    "chars": 495,
    "preview": "┌───────────────────────────────────────────────────────────┐\n│ Name                       Country of Orig…  Dunk-able  "
  },
  {
    "path": "textarea/textarea.go",
    "chars": 52539,
    "preview": "// Package textarea provides a multi-line text input component for Bubble Tea\n// applications.\npackage textarea\n\nimport "
  },
  {
    "path": "textarea/textarea_test.go",
    "chars": 39196,
    "preview": "package textarea\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\t\"unicode\"\n\n\ttea \"charm.land/bubbletea/v2\"\n\t\"charm.land/lipgloss"
  },
  {
    "path": "textinput/styles.go",
    "chars": 2751,
    "preview": "package textinput\n\nimport (\n\t\"image/color\"\n\t\"time\"\n\n\ttea \"charm.land/bubbletea/v2\"\n\t\"charm.land/lipgloss/v2\"\n)\n\n// Defau"
  },
  {
    "path": "textinput/textinput.go",
    "chars": 25331,
    "preview": "// Package textinput provides a text input component for Bubble Tea\n// applications.\npackage textinput\n\nimport (\n\t\"refle"
  },
  {
    "path": "textinput/textinput_test.go",
    "chars": 3176,
    "preview": "package textinput\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\n\ttea \"charm.land/bubbletea/v2\"\n)\n\nfunc Test_Current"
  },
  {
    "path": "timer/timer.go",
    "chars": 5293,
    "preview": "// Package timer provides a simple timeout component.\npackage timer\n\nimport (\n\t\"sync/atomic\"\n\t\"time\"\n\n\ttea \"charm.land/b"
  },
  {
    "path": "viewport/highlight.go",
    "chars": 3136,
    "preview": "package viewport\n\nimport (\n\t\"charm.land/lipgloss/v2\"\n\t\"github.com/charmbracelet/x/ansi\"\n\t\"github.com/rivo/uniseg\"\n)\n\n// "
  },
  {
    "path": "viewport/keymap.go",
    "chars": 1517,
    "preview": "// Package viewport provides a component for rendering a viewport in a Bubble\n// Tea.\npackage viewport\n\nimport \"charm.la"
  },
  {
    "path": "viewport/testdata/TestSizing/view-40x1-softwrap-at-bottom.golden",
    "chars": 128,
    "preview": "╭────────────────────────────────────────╮\n│ll know how many foes you've defeated.  │\n╰─────────────────────────────────"
  },
  {
    "path": "viewport/testdata/TestSizing/view-40x1-softwrap-scrolled-plus-1.golden",
    "chars": 128,
    "preview": "╭────────────────────────────────────────╮\n│cter Zote from an awesome \"Hollow knight│\n╰─────────────────────────────────"
  },
  {
    "path": "viewport/testdata/TestSizing/view-40x1-softwrap-scrolled-plus-2.golden",
    "chars": 128,
    "preview": "╭────────────────────────────────────────╮\n│\" game (https://store.steampowered.com/a│\n╰─────────────────────────────────"
  },
  {
    "path": "viewport/testdata/TestSizing/view-40x1-softwrap.golden",
    "chars": 128,
    "preview": "╭────────────────────────────────────────╮\n│57 Precepts of narcissistic comedy chara│\n╰─────────────────────────────────"
  },
  {
    "path": "viewport/testdata/TestSizing/view-40x1.golden",
    "chars": 128,
    "preview": "╭────────────────────────────────────────╮\n│57 Precepts of narcissistic comedy chara│\n╰─────────────────────────────────"
  },
  {
    "path": "viewport/testdata/TestSizing/view-40x100percent.golden",
    "chars": 778,
    "preview": "╭──────────────────────────────────────╮\n│57 Precepts of narcissistic comedy cha│\n│Precept One: 'Always Win Your Battles"
  },
  {
    "path": "viewport/testdata/TestSizing/view-50x15-content-lines.golden",
    "chars": 764,
    "preview": "57 Precepts of narcissistic comedy character Zote \nawesome \"Hollow knight\" game                      \n                  "
  },
  {
    "path": "viewport/testdata/TestSizing/view-50x15-softwrap-at-bottom.golden",
    "chars": 764,
    "preview": "╭────────────────────────────────────────────────╮\n│Precept Thirteen: 'Never Be Afraid'. Fear can on│\n│ly hold you back."
  },
  {
    "path": "viewport/testdata/TestSizing/view-50x15-softwrap-at-top.golden",
    "chars": 764,
    "preview": "╭────────────────────────────────────────────────╮\n│57 Precepts of narcissistic comedy character Zot│\n│e from an awesome"
  },
  {
    "path": "viewport/testdata/TestSizing/view-50x15-softwrap-gutter-at-bottom.golden",
    "chars": 764,
    "preview": "╭────────────────────────────────────────────────╮\n│  Precept Thirteen: 'Never Be Afraid'. Fear can │\n│  only hold you b"
  },
  {
    "path": "viewport/testdata/TestSizing/view-50x15-softwrap-gutter-at-top.golden",
    "chars": 764,
    "preview": "╭────────────────────────────────────────────────╮\n│  57 Precepts of narcissistic comedy character Z│\n│  ote from an awe"
  },
  {
    "path": "viewport/testdata/TestSizing/view-50x15-softwrap-gutter-scrolled-plus-1.golden",
    "chars": 764,
    "preview": "╭────────────────────────────────────────────────╮\n│  ote from an awesome \"Hollow knight\" game (http│\n│  s://store.steam"
  },
  {
    "path": "viewport/testdata/TestSizing/view-50x15-softwrap-gutter-scrolled-plus-2.golden",
    "chars": 764,
    "preview": "╭────────────────────────────────────────────────╮\n│  s://store.steampowered.com/app/367520/Hollow_K│\n│  night/).       "
  },
  {
    "path": "viewport/testdata/TestSizing/view-50x15-softwrap-scrolled-plus-1.golden",
    "chars": 764,
    "preview": "╭────────────────────────────────────────────────╮\n│e from an awesome \"Hollow knight\" game (https://│\n│store.steampowere"
  },
  {
    "path": "viewport/testdata/TestSizing/view-50x15-softwrap-scrolled-plus-2.golden",
    "chars": 764,
    "preview": "╭────────────────────────────────────────────────╮\n│store.steampowered.com/app/367520/Hollow_Knight/│\n│).               "
  },
  {
    "path": "viewport/viewport.go",
    "chars": 19982,
    "preview": "package viewport\n\nimport (\n\t\"cmp\"\n\t\"math\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"charm.land/bubbles/v2/key\"\n\ttea \"charm.land/bubbletea/"
  },
  {
    "path": "viewport/viewport_test.go",
    "chars": 20558,
    "preview": "package viewport\n\nimport (\n\t\"fmt\"\n\t\"reflect\"\n\t\"regexp\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"charm.land/lipgloss/v2\"\n\t\"github.com/cha"
  }
]

About this extraction

This page contains the full source code of the charmbracelet/bubbles GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 98 files (390.4 KB), approximately 115.7k tokens, and a symbol index with 683 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!