[
  {
    "path": ".github/CODEOWNERS",
    "content": "*  @meowgorithm @bashbunni\ncursor/  @aymanbagabas\nfilepicker/  @bashbunni\nhelp/  @meowgorithm\nkey/  @meowgorithm\nlist/  @meowgorithm\npaginator/  @meowgorithm\nprogress/  @meowgorithm\nspinner/  @meowgorithm\nstopwatch/  @caarlos0\ntable/  @aymanbagabas\ntextarea/  @aymanbagabas\ntextinput/  @meowgorithm\nviewport/  @meowgorithm\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**Setup**\nPlease complete the following information along with version numbers, if applicable.\n - OS [e.g. Ubuntu, macOS]\n - Shell [e.g. zsh, fish]\n - Terminal Emulator [e.g. kitty, iterm]\n - Terminal Multiplexer [e.g. tmux]\n - Locale [e.g. en_US.UTF-8, zh_CN.UTF-8, etc.]\n\n**To Reproduce**\nSteps to reproduce the behavior:\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Source Code**\nPlease include source code if needed to reproduce the behavior. \n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Screenshots**\nAdd screenshots to help explain your problem.\n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "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",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: enhancement\nassignees: ''\n\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\n\nupdates:\n  - package-ecosystem: \"gomod\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n      day: \"monday\"\n      time: \"05:00\"\n      timezone: \"America/New_York\"\n    labels:\n      - \"dependencies\"\n    commit-message:\n      prefix: \"chore\"\n      include: \"scope\"\n    groups:\n      all:\n        patterns:\n          - \"*\"\n\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n      day: \"monday\"\n      time: \"05:00\"\n      timezone: \"America/New_York\"\n    labels:\n      - \"dependencies\"\n    commit-message:\n      prefix: \"chore\"\n      include: \"scope\"\n    groups:\n      all:\n        patterns:\n          - \"*\"\n\n  - package-ecosystem: \"docker\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n      day: \"monday\"\n      time: \"05:00\"\n      timezone: \"America/New_York\"\n    labels:\n      - \"dependencies\"\n    commit-message:\n      prefix: \"chore\"\n      include: \"scope\"\n    groups:\n      all:\n        patterns:\n          - \"*\"\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: build\n\non: [push, pull_request]\n\njobs:\n  test:\n    strategy:\n      matrix:\n        go-version: [stable, oldstable]\n        os: [ubuntu-latest, macos-latest, windows-latest]\n    runs-on: ${{ matrix.os }}\n    steps:\n      - name: Install Go\n        uses: actions/setup-go@v6\n        with:\n          go-version: ${{ matrix.go-version }}\n\n      - name: Checkout code\n        uses: actions/checkout@v6\n\n      - name: Download Go modules\n        run: go mod download\n\n      - name: Build\n        run: go build -v ./...\n\n      - name: Test\n        run: go test ./...\n\n  dependabot:\n    needs: [test]\n    runs-on: ubuntu-latest\n    permissions:\n      pull-requests: write\n      contents: write\n    if: ${{ github.actor == 'dependabot[bot]' && github.event_name == 'pull_request'}}\n    steps:\n      - id: metadata\n        uses: dependabot/fetch-metadata@v2\n        with:\n          github-token: \"${{ secrets.GITHUB_TOKEN }}\"\n      - run: |\n          gh pr review --approve \"$PR_URL\"\n          gh pr merge --squash --auto \"$PR_URL\"\n        env:\n          PR_URL: ${{github.event.pull_request.html_url}}\n          GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}\n"
  },
  {
    "path": ".github/workflows/coverage.yml",
    "content": "name: coverage\non: [push, pull_request]\n\njobs:\n  coverage:\n    strategy:\n      matrix:\n        go-version: [^1.22]\n        os: [ubuntu-latest]\n    runs-on: ${{ matrix.os }}\n    env:\n      GO111MODULE: \"on\"\n    steps:\n      - name: Install Go\n        uses: actions/setup-go@v6\n        with:\n          go-version: ${{ matrix.go-version }}\n\n      - name: Checkout code\n        uses: actions/checkout@v6\n\n      - name: Coverage\n        env:\n          COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        run: |\n          go test -race -covermode atomic -coverprofile=profile.cov ./...\n          go install github.com/mattn/goveralls@latest\n          goveralls -coverprofile=profile.cov -service=github\n"
  },
  {
    "path": ".github/workflows/dependabot-sync.yml",
    "content": "name: dependabot-sync\non:\n  schedule:\n    - cron: \"0 0 * * 0\" # every Sunday at midnight\n  workflow_dispatch: # allows manual triggering\n\npermissions:\n  contents: write\n  pull-requests: write\n\njobs:\n  dependabot-sync:\n    uses: charmbracelet/meta/.github/workflows/dependabot-sync.yml@main\n    with:\n      repo_name: ${{ github.event.repository.name }}\n    secrets:\n      gh_token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/lint-sync.yml",
    "content": "name: lint-sync\non:\n  schedule:\n    # every Sunday at midnight\n    - cron: \"0 0 * * 0\"\n  workflow_dispatch: # allows manual triggering\n\npermissions:\n  contents: write\n  pull-requests: write\n\njobs:\n  lint:\n    uses: charmbracelet/meta/.github/workflows/lint-sync.yml@main\n"
  },
  {
    "path": ".github/workflows/lint.yml",
    "content": "name: lint\non:\n  push:\n  pull_request:\n\njobs:\n  lint:\n    uses: charmbracelet/meta/.github/workflows/lint.yml@main\n    with:\n      golangci_path: .golangci.yml\n      golangci_version: v2.9\n      timeout: 10m\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: goreleaser\n\non:\n  push:\n    tags:\n      - v*.*.*\n\nconcurrency:\n  group: goreleaser\n  cancel-in-progress: true\n\njobs:\n  goreleaser:\n    uses: charmbracelet/meta/.github/workflows/goreleaser.yml@main\n    secrets:\n      docker_username: ${{ secrets.DOCKERHUB_USERNAME }}\n      docker_token: ${{ secrets.DOCKERHUB_TOKEN }}\n      gh_pat: ${{ secrets.PERSONAL_ACCESS_TOKEN }}\n      goreleaser_key: ${{ secrets.GORELEASER_KEY }}\n      twitter_consumer_key: ${{ secrets.TWITTER_CONSUMER_KEY }}\n      twitter_consumer_secret: ${{ secrets.TWITTER_CONSUMER_SECRET }}\n      twitter_access_token: ${{ secrets.TWITTER_ACCESS_TOKEN }}\n      twitter_access_token_secret: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }}\n      mastodon_client_id: ${{ secrets.MASTODON_CLIENT_ID }}\n      mastodon_client_secret: ${{ secrets.MASTODON_CLIENT_SECRET }}\n      mastodon_access_token: ${{ secrets.MASTODON_ACCESS_TOKEN }}\n      discord_webhook_id: ${{ secrets.DISCORD_WEBHOOK_ID }}\n      discord_webhook_token: ${{ secrets.DISCORD_WEBHOOK_TOKEN }}\n# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json\n"
  },
  {
    "path": ".gitignore",
    "content": ".DS_Store\ndist/\n"
  },
  {
    "path": ".golangci.yml",
    "content": "version: \"2\"\nrun:\n  tests: false\nlinters:\n  enable:\n    - bodyclose\n    - exhaustive\n    - goconst\n    - godot\n    - godox\n    - gomoddirectives\n    - goprintffuncname\n    - gosec\n    - misspell\n    - nakedret\n    - nestif\n    - nilerr\n    - noctx\n    - nolintlint\n    - prealloc\n    - revive\n    - rowserrcheck\n    - sqlclosecheck\n    - tparallel\n    - unconvert\n    - unparam\n    - whitespace\n    - wrapcheck\n  exclusions:\n    generated: lax\n    presets:\n      - common-false-positives\n    rules:\n      - linters:\n          - revive\n        text: \"var-naming\"\nissues:\n  max-issues-per-linter: 0\n  max-same-issues: 0\nformatters:\n  enable:\n    - gofumpt\n    - goimports\n  exclusions:\n    generated: lax\n"
  },
  {
    "path": ".goreleaser.yml",
    "content": "# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json\nversion: 2\nincludes:\n  - from_url:\n      url: charmbracelet/meta/main/goreleaser-lib.yaml\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2020-2026 Charmbracelet, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Bubbles\n\n<img src=\"https://github.com/user-attachments/assets/b89fa46e-d451-4b33-a009-c68d4765520f\" width=\"350\" />\n\n[![Latest Release](https://img.shields.io/github/release/charmbracelet/bubbles.svg)](https://github.com/charmbracelet/bubbles/releases)\n[![GoDoc](https://godoc.org/github.com/golang/gddo?status.svg)](https://pkg.go.dev/github.com/charmbracelet/bubbles)\n[![Build Status](https://github.com/charmbracelet/bubbles/workflows/build/badge.svg)](https://github.com/charmbracelet/bubbles/actions)\n[![Go ReportCard](https://goreportcard.com/badge/charmbracelet/bubbles)](https://goreportcard.com/report/charmbracelet/bubbles)\n\nPrimatives for [Bubble Tea](https://github.com/charmbracelet/bubbletea)\napplications. These components are used in production in [Crush][crush], and [many other applications][otherstuff].\n\n> [!TIP]\n>\n> Upgrading from v1? Check out the [upgrade guide](./UPGRADE_GUIDE_V2.md), or\n> point your LLM at it and let it go to town.\n\n[crush]: https://github.com/charmbracelet/crush\n[otherstuff]: https://github.com/charmbracelet/bubbletea/#bubble-tea-in-the-wild\n\n## Spinner\n\n<img src=\"https://stuff.charm.sh/bubbles-examples/spinner.gif\" width=\"400\" alt=\"Spinner Example\">\n\nA spinner, useful for indicating that some kind an operation is happening.\nThere are a couple default ones, but you can also pass your own ”frames.”\n\n- [Example code, basic spinner](https://github.com/charmbracelet/bubbletea/blob/main/examples/spinner/main.go)\n- [Example code, various spinners](https://github.com/charmbracelet/bubbletea/blob/main/examples/spinners/main.go)\n\n## Text Input\n\n<img src=\"https://stuff.charm.sh/bubbles-examples/textinput.gif\" width=\"400\" alt=\"Text Input Example\">\n\nA text input field, akin to an `<input type=\"text\">` in HTML. Supports unicode,\npasting, in-place scrolling when the value exceeds the width of the element and\nthe common, and many customization options.\n\n- [Example code, one field](https://github.com/charmbracelet/bubbletea/blob/main/examples/textinput/main.go)\n- [Example code, many fields](https://github.com/charmbracelet/bubbletea/blob/main/examples/textinputs/main.go)\n\n## Text Area\n\n<img src=\"https://stuff.charm.sh/bubbles-examples/textarea.gif\" width=\"400\" alt=\"Text Area Example\">\n\nA text area field, akin to an `<textarea />` in HTML. Allows for input that\nspans multiple lines. Supports unicode, pasting, vertical scrolling when the\nvalue exceeds the width and height of the element, and many customization\noptions.\n\n- [Example code, chat input](https://github.com/charmbracelet/bubbletea/blob/main/examples/chat/main.go)\n- [Example code, story time input](https://github.com/charmbracelet/bubbletea/blob/main/examples/textarea/main.go)\n\n## Table\n\n<img src=\"https://stuff.charm.sh/bubbles-examples/table.gif\" width=\"400\" alt=\"Table Example\">\n\nA component for displaying and navigating tabular data (columns and rows).\nSupports vertical scrolling and many customization options.\n\n- [Example code, countries and populations](https://github.com/charmbracelet/bubbletea/blob/main/examples/table/main.go)\n\n## Progress\n\n<img src=\"https://stuff.charm.sh/bubbles-examples/progress.gif\" width=\"800\" alt=\"Progressbar Example\">\n\nA simple, customizable progress meter, with optional animation via\n[Harmonica][harmonica]. Supports solid and gradient fills. The empty and filled\nrunes can be set to whatever you'd like. The percentage readout is customizable\nand can also be omitted entirely.\n\n- [Animated example](https://github.com/charmbracelet/bubbletea/blob/main/examples/progress-animated/main.go)\n- [Static example](https://github.com/charmbracelet/bubbletea/blob/main/examples/progress-static/main.go)\n\n[harmonica]: https://github.com/charmbracelet/harmonica\n\n## Paginator\n\n<img src=\"https://stuff.charm.sh/bubbles-examples/pagination.gif\" width=\"200\" alt=\"Paginator Example\">\n\nA component for handling pagination logic and optionally drawing pagination UI.\nSupports \"dot-style\" pagination (similar to what you might see on iOS) and\nnumeric page numbering, but you could also just use this component for the\nlogic and visualize pagination however you like.\n\n- [Example code](https://github.com/charmbracelet/bubbletea/blob/main/examples/paginator/main.go)\n\n## Viewport\n\n<img src=\"https://stuff.charm.sh/bubbles-examples/viewport.gif\" width=\"600\" alt=\"Viewport Example\">\n\nA viewport for vertically scrolling content. Optionally includes standard\npager keybindings and mouse wheel support. A high performance mode is available\nfor applications which make use of the alternate screen buffer.\n\n- [Example code](https://github.com/charmbracelet/bubbletea/blob/main/examples/pager/main.go)\n\nThis component is well complemented with [Reflow][reflow] for ANSI-aware\nindenting and text wrapping.\n\n[reflow]: https://github.com/muesli/reflow\n\n## List\n\n<img src=\"https://stuff.charm.sh/bubbles-examples/list.gif\" width=\"600\" alt=\"List Example\">\n\nA customizable, batteries-included component for browsing a set of items.\nFeatures pagination, fuzzy filtering, auto-generated help, an activity spinner,\nand status messages, all of which can be enabled and disabled as needed.\nExtrapolated from [Glow][glow].\n\n- [Example code, default list](https://github.com/charmbracelet/bubbletea/blob/main/examples/list-default/main.go)\n- [Example code, simple list](https://github.com/charmbracelet/bubbletea/blob/main/examples/list-simple/main.go)\n- [Example code, all features](https://github.com/charmbracelet/bubbletea/blob/main/examples/list-fancy/main.go)\n\n## File Picker\n\n<img src=\"https://vhs.charm.sh/vhs-yET2HNiJNEbyqaVfYuLnY.gif\" width=\"600\" alt=\"File picker example\">\n\nA customizable component for picking a file from the file system. Navigate\nthrough directories and select files, optionally limit to certain file\nextensions.\n\n- [Example code](https://github.com/charmbracelet/bubbletea/blob/main/examples/file-picker/main.go)\n\n## Timer\n\nA simple, flexible component for counting down. The update frequency and output\ncan be customized as you like.\n\n<img src=\"https://stuff.charm.sh/bubbles-examples/timer.gif\" width=\"400\" alt=\"Timer example\">\n\n- [Example code](https://github.com/charmbracelet/bubbletea/blob/main/examples/timer/main.go)\n\n## Stopwatch\n\n<img src=\"https://stuff.charm.sh/bubbles-examples/stopwatch.gif\" width=\"400\" alt=\"Stopwatch example\">\n\nA simple, flexible component for counting up. The update frequency and output\ncan be customized as you see fit.\n\n- [Example code](https://github.com/charmbracelet/bubbletea/blob/main/examples/stopwatch/main.go)\n\n## Help\n\n<img src=\"https://stuff.charm.sh/bubbles-examples/help.gif\" width=\"500\" alt=\"Help Example\">\n\nA customizable horizontal mini help view that automatically generates itself\nfrom your keybindings. It features single and multi-line modes, which the user\ncan optionally toggle between. It will truncate gracefully if the terminal is\ntoo wide for the content.\n\n- [Example code](https://github.com/charmbracelet/bubbletea/blob/main/examples/help/main.go)\n\n## Key\n\nA non-visual component for managing keybindings. It’s useful for allowing users\nto remap keybindings as well as generating help views corresponding to your\nkeybindings.\n\n```go\ntype KeyMap struct {\n    Up key.Binding\n    Down key.Binding\n}\n\nvar DefaultKeyMap = KeyMap{\n    Up: key.NewBinding(\n        key.WithKeys(\"k\", \"up\"),        // actual keybindings\n        key.WithHelp(\"↑/k\", \"move up\"), // corresponding help text\n    ),\n    Down: key.NewBinding(\n        key.WithKeys(\"j\", \"down\"),\n        key.WithHelp(\"↓/j\", \"move down\"),\n    ),\n}\n\nfunc (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {\n    switch msg := msg.(type) {\n    case tea.KeyPressMsg:\n        switch {\n        case key.Matches(msg, DefaultKeyMap.Up):\n            // The user pressed up\n        case key.Matches(msg, DefaultKeyMap.Down):\n            // The user pressed down\n        }\n    }\n    return m, nil\n}\n```\n\n## There’s more where that came from\n\nTo check out community-maintained Bubbles see [Charm & Friends][charmandfriends].\nMade a cool Bubble that you want to share? [PRs][prs] are welcome!\n\n[charmandfriends]: https://github.com/charm-and-friends/additional-bubbles\n[prs]: https://github.com/charm-and-friends/additional-bubbles?tab=readme-ov-file#what-is-a-complete-project\n\n## Contributing\n\nSee [contributing][contribute].\n\n[contribute]: https://github.com/charmbracelet/bubbles/contribute\n\n## Feedback\n\nWe’d love to hear your thoughts on this project. Feel free to drop us a note!\n\n- [Twitter](https://twitter.com/charmcli)\n- [The Fediverse](https://mastodon.social/@charmcli)\n- [Discord](https://charm.sh/chat)\n\n## License\n\n[MIT](https://github.com/charmbracelet/bubbletea/raw/main/LICENSE)\n\n---\n\nPart of [Charm](https://charm.land).\n\n<a href=\"https://charm.sh/\"><img alt=\"The Charm logo\" src=\"https://stuff.charm.sh/charm-banner-next.jpg\" width=\"400\"></a>\n\nCharm热爱开源 • Charm loves open source\n"
  },
  {
    "path": "Taskfile.yaml",
    "content": "# https://taskfile.dev\n\nversion: '3'\n\ntasks:\n  lint:\n    desc: Run lint\n    cmds:\n      - golangci-lint run\n\n  test:\n    desc: Run tests\n    cmds:\n      - go test ./... {{.CLI_ARGS}}\n"
  },
  {
    "path": "UPGRADE_GUIDE_V2.md",
    "content": "# Upgrading to Bubbles v2\n\nThis 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.\n\n> **Companion upgrades required.** Bubbles v2 requires Bubble Tea v2 and Lip Gloss v2. Upgrade all three together:\n>\n> ```sh\n> go get charm.land/bubbletea/v2@latest\n> go get charm.land/bubbles/v2@latest\n> go get charm.land/lipgloss/v2@latest\n> ```\n\n---\n\n## Table of Contents\n\n1. [Import Paths](#1-import-paths)\n2. [Global Patterns](#2-global-patterns)\n3. [Per-Component Migration](#3-per-component-migration)\n   - [Cursor](#cursor)\n   - [Filepicker](#filepicker)\n   - [Help](#help)\n   - [List](#list)\n   - [Paginator](#paginator)\n   - [Progress](#progress)\n   - [Spinner](#spinner)\n   - [Stopwatch](#stopwatch)\n   - [Table](#table)\n   - [Textarea](#textarea)\n   - [Textinput](#textinput)\n   - [Timer](#timer)\n   - [Viewport](#viewport)\n4. [Light and Dark Styles](#4-light-and-dark-styles)\n5. [Removed Symbols Reference](#5-removed-symbols-reference)\n\n---\n\n## 1. Import Paths\n\nReplace all `github.com/charmbracelet/bubbles` imports with `charm.land/bubbles/v2`:\n\n```go\n// Before\nimport (\n    \"github.com/charmbracelet/bubbles/cursor\"\n    \"github.com/charmbracelet/bubbles/help\"\n    \"github.com/charmbracelet/bubbles/key\"\n    \"github.com/charmbracelet/bubbles/list\"\n    \"github.com/charmbracelet/bubbles/paginator\"\n    \"github.com/charmbracelet/bubbles/progress\"\n    \"github.com/charmbracelet/bubbles/runeutil\"\n    \"github.com/charmbracelet/bubbles/spinner\"\n    \"github.com/charmbracelet/bubbles/stopwatch\"\n    \"github.com/charmbracelet/bubbles/table\"\n    \"github.com/charmbracelet/bubbles/textarea\"\n    \"github.com/charmbracelet/bubbles/textinput\"\n    \"github.com/charmbracelet/bubbles/timer\"\n    \"github.com/charmbracelet/bubbles/viewport\"\n)\n\n// After\nimport (\n    \"charm.land/bubbles/v2/cursor\"\n    \"charm.land/bubbles/v2/help\"\n    \"charm.land/bubbles/v2/key\"\n    \"charm.land/bubbles/v2/list\"\n    \"charm.land/bubbles/v2/paginator\"\n    \"charm.land/bubbles/v2/progress\"\n    \"charm.land/bubbles/v2/spinner\"\n    \"charm.land/bubbles/v2/stopwatch\"\n    \"charm.land/bubbles/v2/table\"\n    \"charm.land/bubbles/v2/textarea\"\n    \"charm.land/bubbles/v2/textinput\"\n    \"charm.land/bubbles/v2/timer\"\n    \"charm.land/bubbles/v2/viewport\"\n)\n```\n\n> **Note:** The `runeutil` and `memoization` packages are now internal and no longer importable.\n\n**Search-and-replace pattern:**\n\n```\ngithub.com/charmbracelet/bubbles/  →  charm.land/bubbles/v2/\ngithub.com/charmbracelet/bubbles   →  charm.land/bubbles/v2\n```\n\n---\n\n## 2. Global Patterns\n\nThese patterns repeat across multiple components. Address them first for the broadest impact.\n\n### 2a. `tea.KeyMsg` → `tea.KeyPressMsg`\n\nBubble Tea v2 renames `tea.KeyMsg` to `tea.KeyPressMsg`. All Bubbles that handle key events have been updated. Update your own `Update` functions:\n\n```go\n// Before\ncase tea.KeyMsg:\n\n// After\ncase tea.KeyPressMsg:\n```\n\n### 2b. Exported Width/Height Fields → Getter/Setter Methods\n\nMany components replaced exported `Width` and `Height` fields with methods. The general pattern:\n\n```go\n// Before\nm.Width = 40\nm.Height = 20\nfmt.Println(m.Width, m.Height)\n\n// After\nm.SetWidth(40)\nm.SetHeight(20)\nfmt.Println(m.Width(), m.Height())\n```\n\n**Affected components:** `filepicker`, `help`, `progress`, `table`, `textinput`, `viewport`.\n\n### 2c. `DefaultKeyMap` Variables → Functions\n\nGlobal mutable `DefaultKeyMap` variables are now functions returning fresh values:\n\n```go\n// Before\nkm := textinput.DefaultKeyMap\nkm.Paste.SetEnabled(false)\n\n// After\nkm := textinput.DefaultKeyMap()\nkm.Paste.SetEnabled(false)\n```\n\n**Affected components:** `paginator`, `textarea`, `textinput`.\n\n### 2d. `AdaptiveColor` → `LightDark` with `isDark bool`\n\nLip 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.\n\n### 2e. Removed `NewModel` Aliases\n\nAll `NewModel` variables (deprecated aliases for `New`) have been removed. Use `New` directly.\n\n**Affected components:** `help`, `list`, `paginator`, `spinner`, `textinput`.\n\n---\n\n## 3. Per-Component Migration\n\n### Cursor\n\n| v1 | v2 |\n|----|-----|\n| `model.Blink` | `model.IsBlinked` |\n| `model.BlinkCmd()` | `model.Blink()` |\n\n### Filepicker\n\n| v1 | v2 |\n|----|-----|\n| `DefaultStylesWithRenderer(r)` | `DefaultStyles()` |\n| `model.Height = 10` | `model.SetHeight(10)` |\n| `_ = model.Height` | `_ = model.Height()` |\n\n### Help\n\n| v1 | v2 |\n|----|-----|\n| `model.Width = 80` | `model.SetWidth(80)` |\n| `_ = model.Width` | `_ = model.Width()` |\n| `NewModel()` | `New()` |\n\nNew functions:\n- `DefaultStyles(isDark bool) Styles`\n- `DefaultDarkStyles() Styles`\n- `DefaultLightStyles() Styles`\n\nApply styles explicitly:\n\n```go\n// Before\nh := help.New()\n// Colors auto-adapted to terminal background\n\n// After\nh := help.New()\nh.Styles = help.DefaultStyles(isDark)\n```\n\n### List\n\n| v1 | v2 |\n|----|-----|\n| `DefaultStyles()` | `DefaultStyles(isDark)` |\n| `NewDefaultItemStyles()` | `NewDefaultItemStyles(isDark)` |\n| `styles.FilterPrompt` | `styles.Filter.Focused.Prompt` / `styles.Filter.Blurred.Prompt` |\n| `styles.FilterCursor` | `styles.Filter.Cursor` |\n| `NewModel(...)` | `New(...)` |\n\nThe `Styles.FilterPrompt` and `Styles.FilterCursor` fields have been consolidated into `Styles.Filter`, which is a `textinput.Styles` struct.\n\n### Paginator\n\n| v1 | v2 |\n|----|-----|\n| `DefaultKeyMap` (var) | `DefaultKeyMap()` (func) |\n| `model.UsePgUpPgDownKeys` | Removed — customize `KeyMap` directly |\n| `model.UseLeftRightKeys` | Removed — customize `KeyMap` directly |\n| `model.UseUpDownKeys` | Removed — customize `KeyMap` directly |\n| `model.UseHLKeys` | Removed — customize `KeyMap` directly |\n| `model.UseJKKeys` | Removed — customize `KeyMap` directly |\n| `NewModel(...)` | `New(...)` |\n\n### Progress\n\nThis component has the most extensive changes.\n\n#### Width\n\n```go\n// Before\np.Width = 40\nfmt.Println(p.Width)\n\n// After\np.SetWidth(40)\nfmt.Println(p.Width())\n```\n\n#### Colors\n\nColor types changed from `string` to `image/color.Color`:\n\n```go\n// Before\np.FullColor = \"#FF0000\"\np.EmptyColor = \"#333333\"\n\n// After\np.FullColor = lipgloss.Color(\"#FF0000\")\np.EmptyColor = lipgloss.Color(\"#333333\")\n```\n\n#### Gradient/Blend Options\n\n```go\n// Before\nprogress.New(progress.WithGradient(\"#5A56E0\", \"#EE6FF8\"))\nprogress.New(progress.WithDefaultGradient())\nprogress.New(progress.WithScaledGradient(\"#5A56E0\", \"#EE6FF8\"))\nprogress.New(progress.WithDefaultScaledGradient())\nprogress.New(progress.WithSolidFill(\"#7571F9\"))\n\n// After\nprogress.New(progress.WithColors(lipgloss.Color(\"#5A56E0\"), lipgloss.Color(\"#EE6FF8\")))\nprogress.New(progress.WithDefaultBlend())\nprogress.New(progress.WithColors(lipgloss.Color(\"#5A56E0\"), lipgloss.Color(\"#EE6FF8\")), progress.WithScaled(true))\nprogress.New(progress.WithDefaultBlend(), progress.WithScaled(true))\nprogress.New(progress.WithColors(lipgloss.Color(\"#7571F9\")))\n```\n\n| v1 | v2 |\n|----|-----|\n| `WithGradient(a, b string)` | `WithColors(colors ...color.Color)` |\n| `WithDefaultGradient()` | `WithDefaultBlend()` |\n| `WithScaledGradient(a, b string)` | `WithColors(...) + WithScaled(true)` |\n| `WithDefaultScaledGradient()` | `WithDefaultBlend() + WithScaled(true)` |\n| `WithSolidFill(string)` | `WithColors(color)` (single color) |\n| `WithColorProfile(termenv.Profile)` | Removed (automatic) |\n| `Update() (tea.Model, tea.Cmd)` | `Update() (Model, tea.Cmd)` |\n\nNew options:\n- `WithColorFunc(func(total, current float64) color.Color)` — dynamic per-cell coloring\n- `WithScaled(bool)` — scale blend to filled portion\n\n### Spinner\n\n| v1 | v2 |\n|----|-----|\n| `NewModel()` | `New()` |\n| `spinner.Tick()` (package func) | `model.Tick()` (method) |\n\n### Stopwatch\n\n```go\n// Before\nsw := stopwatch.NewWithInterval(500 * time.Millisecond)\n\n// After\nsw := stopwatch.New(stopwatch.WithInterval(500 * time.Millisecond))\n```\n\n| v1 | v2 |\n|----|-----|\n| `NewWithInterval(d)` | `New(WithInterval(d))` |\n\n### Table\n\n| v1 | v2 |\n|----|-----|\n| `model.viewport.Width` | `model.Width()` / `model.SetWidth(w)` |\n| `model.viewport.Height` | `model.Height()` / `model.SetHeight(h)` |\n\nThe table already had `SetWidth`/`SetHeight`/`Width()`/`Height()` in v1, but internally these now use viewport getter/setters.\n\n### Textarea\n\n#### KeyMap\n\n```go\n// Before\nkm := textarea.DefaultKeyMap\n// After\nkm := textarea.DefaultKeyMap()\n```\n\nNew key bindings added: `PageUp`, `PageDown`.\n\n#### Styles\n\nThe styling system has been restructured:\n\n```go\n// Before\nta := textarea.New()\nta.FocusedStyle.Base = lipgloss.NewStyle().Border(lipgloss.RoundedBorder())\nta.BlurredStyle.Base = lipgloss.NewStyle().Border(lipgloss.HiddenBorder())\n\n// After\nta := textarea.New()\n// Styles are now nested under a Styles struct\n// Access via Styles.Focused and Styles.Blurred (type StyleState)\n```\n\n| v1 | v2 |\n|----|-----|\n| `textarea.Style` (type) | `textarea.StyleState` (type) |\n| `model.FocusedStyle` | `model.Styles.Focused` |\n| `model.BlurredStyle` | `model.Styles.Blurred` |\n| `DefaultStyles() (focused, blurred Style)` | `DefaultStyles(isDark bool) Styles` |\n\n#### Cursor\n\n```go\n// Before\nta.Cursor                           // cursor.Model (virtual cursor)\nta.SetCursor(col)                   // set cursor column\n\n// After\nta.Cursor()                         // func() *tea.Cursor (real cursor)\nta.SetCursorColumn(col)             // renamed for clarity\nta.VirtualCursor                    // bool: true = virtual, false = real\nta.Styles.Cursor                    // CursorStyle for cursor appearance\n```\n\nNew additions:\n- `Column()` — returns current cursor column (0-indexed)\n- `ScrollYOffset()` — returns vertical scroll offset\n- `ScrollPosition()` — returns scroll position\n- `MoveToBeginning()` / `MoveToEnd()` — navigate to start/end\n\n### Textinput\n\n#### KeyMap\n\n```go\n// Before\nkm := textinput.DefaultKeyMap\n// After\nkm := textinput.DefaultKeyMap()\n```\n\n#### Width\n\n```go\n// Before\nti.Width = 40\n// After\nti.SetWidth(40)\n```\n\n#### Styles\n\nIndividual style fields have moved into a `Styles` struct:\n\n```go\n// Before\nti.PromptStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(\"63\"))\nti.TextStyle = lipgloss.NewStyle()\nti.PlaceholderStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(\"240\"))\nti.CompletionStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(\"240\"))\n\n// After\ns := textinput.DefaultStyles(isDark)\ns.Focused.Prompt = lipgloss.NewStyle().Foreground(lipgloss.Color(\"63\"))\ns.Focused.Text = lipgloss.NewStyle()\ns.Focused.Placeholder = lipgloss.NewStyle().Foreground(lipgloss.Color(\"240\"))\ns.Focused.Suggestion = lipgloss.NewStyle().Foreground(lipgloss.Color(\"240\"))\nti.SetStyles(s)\n```\n\n| v1 Field | v2 Location |\n|----------|-------------|\n| `Model.PromptStyle` | `StyleState.Prompt` |\n| `Model.TextStyle` | `StyleState.Text` |\n| `Model.PlaceholderStyle` | `StyleState.Placeholder` |\n| `Model.CompletionStyle` | `StyleState.Suggestion` |\n| `Model.CursorStyle` | `Styles.Cursor` |\n| `Model.Cursor` (cursor.Model) | `Model.Cursor()` (func → *tea.Cursor) |\n\nNew:\n- `Model.Styles()` / `Model.SetStyles(Styles)` — get/set styles\n- `Model.VirtualCursor()` / `Model.SetVirtualCursor(bool)` — toggle cursor mode\n\n### Timer\n\n```go\n// Before\nt := timer.NewWithInterval(30*time.Second, 100*time.Millisecond)\nt := timer.New(30 * time.Second)\n\n// After\nt := timer.New(30*time.Second, timer.WithInterval(100*time.Millisecond))\nt := timer.New(30 * time.Second)\n```\n\n| v1 | v2 |\n|----|-----|\n| `NewWithInterval(timeout, interval)` | `New(timeout, WithInterval(interval))` |\n\n### Viewport\n\nThis component has the most new features alongside its breaking changes.\n\n#### Constructor\n\n```go\n// Before\nvp := viewport.New(80, 24)\n\n// After\nvp := viewport.New(viewport.WithWidth(80), viewport.WithHeight(24))\n// or\nvp := viewport.New()\nvp.SetWidth(80)\nvp.SetHeight(24)\n```\n\n#### Width, Height, YOffset\n\n```go\n// Before\nvp.Width = 80\nvp.Height = 24\nvp.YOffset = 5\nfmt.Println(vp.Width, vp.Height, vp.YOffset)\n\n// After\nvp.SetWidth(80)\nvp.SetHeight(24)\nvp.SetYOffset(5)\nfmt.Println(vp.Width(), vp.Height(), vp.YOffset())\n```\n\n#### Removed\n\n- `HighPerformanceRendering` — removed entirely (deprecated in Bubble Tea v2)\n\n#### New Features (non-breaking)\n\nThese are additions you can adopt incrementally:\n\n- **Soft wrapping:** `vp.SoftWrap = true`\n- **Left gutter** for line numbers:\n  ```go\n  vp.LeftGutterFunc = func(info viewport.GutterContext) string {\n      if info.Soft { return \"     │ \" }\n      if info.Index >= info.TotalLines { return \"   ~ │ \" }\n      return fmt.Sprintf(\"%4d │ \", info.Index+1)\n  }\n  ```\n- **Highlighting:**\n  ```go\n  vp.SetHighlights(regexp.MustCompile(\"pattern\").FindAllStringIndex(vp.GetContent(), -1))\n  vp.HighlightNext()\n  vp.HighlightPrevious()\n  vp.ClearHighlights()\n  ```\n- **`SetContentLines([]string)`** — set lines directly with virtual soft-wrap support\n- **`GetContent() string`** — retrieve content\n- **`FillHeight bool`** — fill viewport with empty lines\n- **`StyleLineFunc func(int) lipgloss.Style`** — per-line styling\n- **Horizontal scrolling** with left/right arrow keys\n- **Horizontal mouse wheel scrolling**\n\n---\n\n## 4. Light and Dark Styles\n\nLip Gloss v2 removes `AdaptiveColor`, so Bubbles no longer auto-detect terminal background. You must explicitly choose light or dark styles.\n\n### Recommended: Query via Bubble Tea\n\n```go\nfunc (m model) Init() tea.Cmd {\n    return tea.RequestBackgroundColor\n}\n\nfunc (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {\n    switch msg := msg.(type) {\n    case tea.BackgroundColorMsg:\n        isDark := msg.IsDark()\n        m.help.Styles = help.DefaultStyles(isDark)\n        m.list.Styles = list.DefaultStyles(isDark)\n        // ... apply to other components\n    }\n    return m, nil\n}\n```\n\nThis is required when using [Wish](https://github.com/charmbracelet/wish) to detect the client's background.\n\n### Quick: Use `compat` Package\n\n```go\nimport \"charm.land/lipgloss/v2/compat\"\n\nvar isDark = compat.HasDarkBackground()\n\nfunc main() {\n    h := help.New()\n    h.Styles = help.DefaultStyles(isDark)\n}\n```\n\n> **Warning:** The `compat` approach uses blocking I/O outside Bubble Tea's event loop and will not detect remote client backgrounds over SSH.\n\n### Manual\n\n```go\nh.Styles = help.DefaultDarkStyles()   // force dark\nh.Styles = help.DefaultLightStyles()  // force light\n```\n\n---\n\n## 5. Removed Symbols Reference\n\nQuick-reference table of all removed symbols and their replacements:\n\n| Package | Removed | Replacement |\n|---------|---------|-------------|\n| `cursor` | `Model.Blink` | `Model.IsBlinked` |\n| `cursor` | `Model.BlinkCmd()` | `Model.Blink()` |\n| `filepicker` | `DefaultStylesWithRenderer(r)` | `DefaultStyles()` |\n| `filepicker` | `Model.Height` (field) | `Model.SetHeight()` / `Model.Height()` |\n| `help` | `NewModel` | `New()` |\n| `help` | `Model.Width` (field) | `Model.SetWidth()` / `Model.Width()` |\n| `list` | `NewModel` | `New()` |\n| `list` | `DefaultStyles()` | `DefaultStyles(isDark)` |\n| `list` | `NewDefaultItemStyles()` | `NewDefaultItemStyles(isDark)` |\n| `list` | `Styles.FilterPrompt` | `Styles.Filter` (`textinput.Styles`) |\n| `list` | `Styles.FilterCursor` | `Styles.Filter.Cursor` |\n| `paginator` | `DefaultKeyMap` (var) | `DefaultKeyMap()` (func) |\n| `paginator` | `NewModel` | `New()` |\n| `paginator` | `UsePgUpPgDownKeys` etc. | Customize `KeyMap` directly |\n| `progress` | `WithGradient(a, b)` | `WithColors(colors...)` |\n| `progress` | `WithDefaultGradient()` | `WithDefaultBlend()` |\n| `progress` | `WithScaledGradient(a, b)` | `WithColors(...) + WithScaled(true)` |\n| `progress` | `WithDefaultScaledGradient()` | `WithDefaultBlend() + WithScaled(true)` |\n| `progress` | `WithSolidFill(string)` | `WithColors(color)` |\n| `progress` | `WithColorProfile(p)` | Removed (automatic) |\n| `progress` | `Model.Width` (field) | `Model.SetWidth()` / `Model.Width()` |\n| `spinner` | `NewModel` | `New()` |\n| `spinner` | `Tick()` (package func) | `Model.Tick()` |\n| `stopwatch` | `NewWithInterval(d)` | `New(WithInterval(d))` |\n| `table` | `Model.Width` (field) | `Model.SetWidth()` / `Model.Width()` |\n| `table` | `Model.Height` (field) | `Model.SetHeight()` / `Model.Height()` |\n| `textarea` | `DefaultKeyMap` (var) | `DefaultKeyMap()` (func) |\n| `textarea` | `Style` (type) | `StyleState` (type) |\n| `textarea` | `Model.FocusedStyle` | `Model.Styles.Focused` |\n| `textarea` | `Model.BlurredStyle` | `Model.Styles.Blurred` |\n| `textarea` | `Model.SetCursor(col)` | `Model.SetCursorColumn(col)` |\n| `textarea` | `DefaultStyles()` | `DefaultStyles(isDark)` |\n| `textinput` | `DefaultKeyMap` (var) | `DefaultKeyMap()` (func) |\n| `textinput` | `NewModel` | `New()` |\n| `textinput` | `Model.Width` (field) | `Model.SetWidth()` / `Model.Width()` |\n| `textinput` | `Model.PromptStyle` | `StyleState.Prompt` |\n| `textinput` | `Model.TextStyle` | `StyleState.Text` |\n| `textinput` | `Model.PlaceholderStyle` | `StyleState.Placeholder` |\n| `textinput` | `Model.CompletionStyle` | `StyleState.Suggestion` |\n| `textinput` | `Model.CursorStyle` | `Styles.Cursor` |\n| `textinput` | `Model.Cursor` (cursor.Model) | `Model.Cursor()` (func → *tea.Cursor) |\n| `timer` | `NewWithInterval(t, i)` | `New(t, WithInterval(i))` |\n| `viewport` | `New(w, h int)` | `New(...Option)` |\n| `viewport` | `Model.Width` (field) | `Model.SetWidth()` / `Model.Width()` |\n| `viewport` | `Model.Height` (field) | `Model.SetHeight()` / `Model.Height()` |\n| `viewport` | `Model.YOffset` (field) | `Model.SetYOffset()` / `Model.YOffset()` |\n| `viewport` | `HighPerformanceRendering` | Removed |\n| `runeutil` | Entire package | Moved to `internal/runeutil` (not importable) |\n\n---\n\nPart of [Charm](https://charm.land).\n\n<a href=\"https://charm.land/\"><img alt=\"The Charm logo\" src=\"https://stuff.charm.sh/charm-badge.jpg\" width=\"400\"></a>\n"
  },
  {
    "path": "bubbles.go",
    "content": "// Package bubbles provides some components for Bubble Tea applications. These\n// components are used in production in Glow, Charm and many other\n// applications.\npackage bubbles\n"
  },
  {
    "path": "cursor/cursor.go",
    "content": "// Package cursor provides a virtual cursor to support the textinput and\n// textarea elements.\npackage cursor\n\nimport (\n\t\"context\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\ttea \"charm.land/bubbletea/v2\"\n\t\"charm.land/lipgloss/v2\"\n)\n\nconst defaultBlinkSpeed = time.Millisecond * 530\n\n// Internal ID management. Used during animating to ensure that frame messages\n// are received only by spinner components that sent them.\nvar lastID int64\n\nfunc nextID() int {\n\treturn int(atomic.AddInt64(&lastID, 1))\n}\n\n// initialBlinkMsg initializes cursor blinking.\ntype initialBlinkMsg struct{}\n\n// BlinkMsg signals that the cursor should blink. It contains metadata that\n// allows us to tell if the blink message is the one we're expecting.\ntype BlinkMsg struct {\n\tid  int\n\ttag int\n}\n\n// blinkCanceled is sent when a blink operation is canceled.\ntype blinkCanceled struct{}\n\n// blinkCtx manages cursor blinking.\ntype blinkCtx struct {\n\tctx    context.Context\n\tcancel context.CancelFunc\n}\n\n// Mode describes the behavior of the cursor.\ntype Mode int\n\n// Available cursor modes.\nconst (\n\tCursorBlink Mode = iota\n\tCursorStatic\n\tCursorHide\n)\n\n// String returns the cursor mode in a human-readable format. This method is\n// provisional and for informational purposes only.\nfunc (c Mode) String() string {\n\treturn [...]string{\n\t\t\"blink\",\n\t\t\"static\",\n\t\t\"hidden\",\n\t}[c]\n}\n\n// Model is the Bubble Tea model for this cursor element.\ntype Model struct {\n\t// Style styles the cursor block.\n\tStyle lipgloss.Style\n\n\t// TextStyle is the style used for the cursor when it is blinking\n\t// (hidden), i.e. displaying normal text.\n\tTextStyle lipgloss.Style\n\n\t// BlinkSpeed is the speed at which the cursor blinks. This has no effect\n\t// unless [CursorMode] is not set to [CursorBlink].\n\tBlinkSpeed time.Duration\n\n\t// IsBlinked is the state of the cursor blink. When true, the cursor is\n\t// hidden.\n\tIsBlinked bool\n\n\t// char is the character under the cursor\n\tchar string\n\n\t// The ID of this Model as it relates to other cursors\n\tid int\n\n\t// focus indicates whether the containing input is focused\n\tfocus bool\n\n\t// Used to manage cursor blink\n\tblinkCtx *blinkCtx\n\n\t// The ID of the blink message we're expecting to receive.\n\tblinkTag int\n\n\t// mode determines the behavior of the cursor\n\tmode Mode\n}\n\n// New creates a new model with default settings.\nfunc New() Model {\n\treturn Model{\n\t\tid:         nextID(),\n\t\tBlinkSpeed: defaultBlinkSpeed,\n\t\tIsBlinked:  true,\n\t\tmode:       CursorBlink,\n\n\t\tblinkCtx: &blinkCtx{\n\t\t\tctx: context.Background(),\n\t\t},\n\t}\n}\n\n// Update updates the cursor.\nfunc (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {\n\tswitch msg := msg.(type) {\n\tcase initialBlinkMsg:\n\t\t// We accept all initialBlinkMsgs generated by the Blink command.\n\n\t\tif m.mode != CursorBlink || !m.focus {\n\t\t\treturn m, nil\n\t\t}\n\n\t\tcmd := m.Blink()\n\t\treturn m, cmd\n\n\tcase tea.FocusMsg:\n\t\treturn m, m.Focus()\n\n\tcase tea.BlurMsg:\n\t\tm.Blur()\n\t\treturn m, nil\n\n\tcase BlinkMsg:\n\t\t// We're choosy about whether to accept blinkMsgs so that our cursor\n\t\t// only exactly when it should.\n\n\t\t// Is this model blink-able?\n\t\tif m.mode != CursorBlink || !m.focus {\n\t\t\treturn m, nil\n\t\t}\n\n\t\t// Were we expecting this blink message?\n\t\tif msg.id != m.id || msg.tag != m.blinkTag {\n\t\t\treturn m, nil\n\t\t}\n\n\t\tvar cmd tea.Cmd\n\t\tif m.mode == CursorBlink {\n\t\t\tm.IsBlinked = !m.IsBlinked\n\t\t\tcmd = m.Blink()\n\t\t}\n\t\treturn m, cmd\n\n\tcase blinkCanceled: // no-op\n\t\treturn m, nil\n\t}\n\treturn m, nil\n}\n\n// Mode returns the model's cursor mode. For available cursor modes, see\n// type Mode.\nfunc (m Model) Mode() Mode {\n\treturn m.mode\n}\n\n// SetMode sets the model's cursor mode. This method returns a command.\n//\n// For available cursor modes, see type CursorMode.\nfunc (m *Model) SetMode(mode Mode) tea.Cmd {\n\t// Adjust the mode value if it's value is out of range\n\tif mode < CursorBlink || mode > CursorHide {\n\t\treturn nil\n\t}\n\tm.mode = mode\n\tm.IsBlinked = m.mode == CursorHide || !m.focus\n\tif mode == CursorBlink {\n\t\treturn Blink\n\t}\n\treturn nil\n}\n\n// Blink is a command used to manage cursor blinking.\nfunc (m *Model) Blink() tea.Cmd {\n\tif m.mode != CursorBlink {\n\t\treturn nil\n\t}\n\n\tif m.blinkCtx != nil && m.blinkCtx.cancel != nil {\n\t\tm.blinkCtx.cancel()\n\t}\n\n\tctx, cancel := context.WithTimeout(m.blinkCtx.ctx, m.BlinkSpeed)\n\tm.blinkCtx.cancel = cancel\n\n\tm.blinkTag++\n\tblinkMsg := BlinkMsg{id: m.id, tag: m.blinkTag}\n\n\treturn func() tea.Msg {\n\t\tdefer cancel()\n\t\t<-ctx.Done()\n\t\tif ctx.Err() == context.DeadlineExceeded {\n\t\t\treturn blinkMsg\n\t\t}\n\t\treturn blinkCanceled{}\n\t}\n}\n\n// Blink is a command used to initialize cursor blinking.\nfunc Blink() tea.Msg {\n\treturn initialBlinkMsg{}\n}\n\n// Focus focuses the cursor to allow it to blink if desired.\nfunc (m *Model) Focus() tea.Cmd {\n\tm.focus = true\n\tm.IsBlinked = m.mode == CursorHide // show the cursor unless we've explicitly hidden it\n\n\tif m.mode == CursorBlink && m.focus {\n\t\treturn m.Blink()\n\t}\n\treturn nil\n}\n\n// Blur blurs the cursor.\nfunc (m *Model) Blur() {\n\tm.focus = false\n\tm.IsBlinked = true\n}\n\n// SetChar sets the character under the cursor.\nfunc (m *Model) SetChar(char string) {\n\tm.char = char\n}\n\n// View displays the cursor.\nfunc (m Model) View() string {\n\tif m.IsBlinked {\n\t\treturn m.TextStyle.Inline(true).Render(m.char)\n\t}\n\treturn m.Style.Inline(true).Reverse(true).Render(m.char)\n}\n"
  },
  {
    "path": "cursor/cursor_test.go",
    "content": "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//\n// The original [Model.Blink] implementation returned a closure over the pointer receiver:\n//\n//\treturn func() tea.Msg {\n//\t\tdefer cancel()\n//\t\t<-ctx.Done()\n//\t\tif ctx.Err() == context.DeadlineExceeded {\n//\t\t\treturn BlinkMsg{id: m.id, tag: m.blinkTag}\n//\t\t}\n//\t\treturn blinkCanceled{}\n//\t}\n//\n// A race on “m.blinkTag” will occur if:\n//  1. [Model.Blink] is called e.g. by calling [Model.Focus] from\n//     [\"charm.land/bubbletea/v2\".Model.Update];\n//  2. [\"charm.land/bubbletea/v2\".handleCommands] is kept sufficiently busy that it does not receive and\n//     execute the [Model.BlinkCmd] e.g. by other long running command or commands;\n//  3. at least [Mode.BlinkSpeed] time elapses;\n//  4. [Model.Blink] is called again;\n//  5. [\"charm.land/bubbletea/v2\".handleCommands] gets around to receiving and executing the original\n//     closure.\n//\n// Even if this did not formally race, the value of the tag fetched would be semantically incorrect (likely being the\n// current value rather than the value at the time the closure was created).\nfunc TestBlinkCmdDataRace(t *testing.T) {\n\tm := New()\n\tcmd := m.Blink()\n\tvar wg sync.WaitGroup\n\twg.Add(2)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\ttime.Sleep(m.BlinkSpeed * 3)\n\t\tcmd()\n\t}()\n\tgo func() {\n\t\tdefer wg.Done()\n\t\ttime.Sleep(m.BlinkSpeed * 2)\n\t\tm.Blink()\n\t}()\n\twg.Wait()\n}\n"
  },
  {
    "path": "filepicker/filepicker.go",
    "content": "// Package filepicker provides a file picker component for Bubble Tea\n// applications.\npackage filepicker\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync/atomic\"\n\n\t\"charm.land/bubbles/v2/key\"\n\ttea \"charm.land/bubbletea/v2\"\n\t\"charm.land/lipgloss/v2\"\n\t\"github.com/dustin/go-humanize\"\n)\n\nvar lastID int64\n\nfunc nextID() int {\n\treturn int(atomic.AddInt64(&lastID, 1))\n}\n\n// New returns a new filepicker model with default styling and key bindings.\nfunc New() Model {\n\treturn Model{\n\t\tid:               nextID(),\n\t\tCurrentDirectory: \".\",\n\t\tCursor:           \">\",\n\t\tAllowedTypes:     []string{},\n\t\tselected:         0,\n\t\tShowPermissions:  true,\n\t\tShowSize:         true,\n\t\tShowHidden:       false,\n\t\tDirAllowed:       false,\n\t\tFileAllowed:      true,\n\t\tAutoHeight:       true,\n\t\theight:           0,\n\t\tmaxIdx:           0,\n\t\tminIdx:           0,\n\t\tselectedStack:    newStack(),\n\t\tminStack:         newStack(),\n\t\tmaxStack:         newStack(),\n\t\tKeyMap:           DefaultKeyMap(),\n\t\tStyles:           DefaultStyles(),\n\t}\n}\n\ntype errorMsg struct {\n\terr error\n}\n\ntype readDirMsg struct {\n\tid      int\n\tentries []os.DirEntry\n}\n\nconst (\n\tmarginBottom  = 5\n\tfileSizeWidth = 7\n\tpaddingLeft   = 2\n)\n\n// KeyMap defines key bindings for each user action.\ntype KeyMap struct {\n\tGoToTop  key.Binding\n\tGoToLast key.Binding\n\tDown     key.Binding\n\tUp       key.Binding\n\tPageUp   key.Binding\n\tPageDown key.Binding\n\tBack     key.Binding\n\tOpen     key.Binding\n\tSelect   key.Binding\n}\n\n// DefaultKeyMap defines the default keybindings.\nfunc DefaultKeyMap() KeyMap {\n\treturn KeyMap{\n\t\tGoToTop:  key.NewBinding(key.WithKeys(\"g\"), key.WithHelp(\"g\", \"first\")),\n\t\tGoToLast: key.NewBinding(key.WithKeys(\"G\"), key.WithHelp(\"G\", \"last\")),\n\t\tDown:     key.NewBinding(key.WithKeys(\"j\", \"down\", \"ctrl+n\"), key.WithHelp(\"j\", \"down\")),\n\t\tUp:       key.NewBinding(key.WithKeys(\"k\", \"up\", \"ctrl+p\"), key.WithHelp(\"k\", \"up\")),\n\t\tPageUp:   key.NewBinding(key.WithKeys(\"K\", \"pgup\"), key.WithHelp(\"pgup\", \"page up\")),\n\t\tPageDown: key.NewBinding(key.WithKeys(\"J\", \"pgdown\"), key.WithHelp(\"pgdown\", \"page down\")),\n\t\tBack:     key.NewBinding(key.WithKeys(\"h\", \"backspace\", \"left\", \"esc\"), key.WithHelp(\"h\", \"back\")),\n\t\tOpen:     key.NewBinding(key.WithKeys(\"l\", \"right\", \"enter\"), key.WithHelp(\"l\", \"open\")),\n\t\tSelect:   key.NewBinding(key.WithKeys(\"enter\"), key.WithHelp(\"enter\", \"select\")),\n\t}\n}\n\n// Styles defines the possible customizations for styles in the file picker.\ntype Styles struct {\n\tDisabledCursor   lipgloss.Style\n\tCursor           lipgloss.Style\n\tSymlink          lipgloss.Style\n\tDirectory        lipgloss.Style\n\tFile             lipgloss.Style\n\tDisabledFile     lipgloss.Style\n\tPermission       lipgloss.Style\n\tSelected         lipgloss.Style\n\tDisabledSelected lipgloss.Style\n\tFileSize         lipgloss.Style\n\tEmptyDirectory   lipgloss.Style\n}\n\n// DefaultStyles defines the default styling for the file picker.\nfunc DefaultStyles() Styles {\n\treturn Styles{\n\t\tDisabledCursor:   lipgloss.NewStyle().Foreground(lipgloss.Color(\"247\")),\n\t\tCursor:           lipgloss.NewStyle().Foreground(lipgloss.Color(\"212\")),\n\t\tSymlink:          lipgloss.NewStyle().Foreground(lipgloss.Color(\"36\")),\n\t\tDirectory:        lipgloss.NewStyle().Foreground(lipgloss.Color(\"99\")),\n\t\tFile:             lipgloss.NewStyle(),\n\t\tDisabledFile:     lipgloss.NewStyle().Foreground(lipgloss.Color(\"243\")),\n\t\tDisabledSelected: lipgloss.NewStyle().Foreground(lipgloss.Color(\"247\")),\n\t\tPermission:       lipgloss.NewStyle().Foreground(lipgloss.Color(\"244\")),\n\t\tSelected:         lipgloss.NewStyle().Foreground(lipgloss.Color(\"212\")).Bold(true),\n\t\tFileSize:         lipgloss.NewStyle().Foreground(lipgloss.Color(\"240\")).Width(fileSizeWidth).Align(lipgloss.Right),\n\t\tEmptyDirectory:   lipgloss.NewStyle().Foreground(lipgloss.Color(\"240\")).PaddingLeft(paddingLeft).SetString(\"Bummer. No Files Found.\"),\n\t}\n}\n\n// Model represents a file picker.\ntype Model struct {\n\tid int\n\n\t// Path is the path which the user has selected with the file picker.\n\tPath string\n\n\t// CurrentDirectory is the directory that the user is currently in.\n\tCurrentDirectory string\n\n\t// AllowedTypes specifies which file types the user may select.\n\t// If empty the user may select any file.\n\tAllowedTypes []string\n\n\tKeyMap          KeyMap\n\tfiles           []os.DirEntry\n\tShowPermissions bool\n\tShowSize        bool\n\tShowHidden      bool\n\tDirAllowed      bool\n\tFileAllowed     bool\n\n\tFileSelected  string\n\tselected      int\n\tselectedStack stack\n\n\tminIdx   int\n\tmaxIdx   int\n\tmaxStack stack\n\tminStack stack\n\n\theight     int\n\tAutoHeight bool\n\n\tCursor string\n\tStyles Styles\n}\n\ntype stack struct {\n\tPush   func(int)\n\tPop    func() int\n\tLength func() int\n}\n\nfunc newStack() stack {\n\tslice := make([]int, 0)\n\treturn stack{\n\t\tPush: func(i int) {\n\t\t\tslice = append(slice, i)\n\t\t},\n\t\tPop: func() int {\n\t\t\tres := slice[len(slice)-1]\n\t\t\tslice = slice[:len(slice)-1]\n\t\t\treturn res\n\t\t},\n\t\tLength: func() int {\n\t\t\treturn len(slice)\n\t\t},\n\t}\n}\n\nfunc (m *Model) pushView(selected, minimum, maximum int) {\n\tm.selectedStack.Push(selected)\n\tm.minStack.Push(minimum)\n\tm.maxStack.Push(maximum)\n}\n\nfunc (m *Model) popView() (int, int, int) {\n\treturn m.selectedStack.Pop(), m.minStack.Pop(), m.maxStack.Pop()\n}\n\nfunc (m Model) readDir(path string, showHidden bool) tea.Cmd {\n\treturn func() tea.Msg {\n\t\tdirEntries, err := os.ReadDir(path)\n\t\tif err != nil {\n\t\t\treturn errorMsg{err}\n\t\t}\n\n\t\tsort.Slice(dirEntries, func(i, j int) bool {\n\t\t\tif dirEntries[i].IsDir() == dirEntries[j].IsDir() {\n\t\t\t\treturn dirEntries[i].Name() < dirEntries[j].Name()\n\t\t\t}\n\t\t\treturn dirEntries[i].IsDir()\n\t\t})\n\n\t\tif showHidden {\n\t\t\treturn readDirMsg{id: m.id, entries: dirEntries}\n\t\t}\n\n\t\tvar sanitizedDirEntries []os.DirEntry\n\t\tfor _, dirEntry := range dirEntries {\n\t\t\tisHidden, _ := IsHidden(dirEntry.Name())\n\t\t\tif isHidden {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tsanitizedDirEntries = append(sanitizedDirEntries, dirEntry)\n\t\t}\n\t\treturn readDirMsg{id: m.id, entries: sanitizedDirEntries}\n\t}\n}\n\n// SetHeight sets the height of the file picker.\nfunc (m *Model) SetHeight(h int) {\n\tm.height = h\n\tif m.maxIdx > m.height-1 {\n\t\tm.maxIdx = m.minIdx + m.height - 1\n\t}\n}\n\n// Height returns the height of the file picker.\nfunc (m Model) Height() int {\n\treturn m.height\n}\n\n// Init initializes the file picker model.\nfunc (m Model) Init() tea.Cmd {\n\treturn m.readDir(m.CurrentDirectory, m.ShowHidden)\n}\n\n// Update handles user interactions within the file picker model.\nfunc (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {\n\tswitch msg := msg.(type) {\n\tcase readDirMsg:\n\t\tif msg.id != m.id {\n\t\t\tbreak\n\t\t}\n\t\tm.files = msg.entries\n\t\tm.maxIdx = max(m.maxIdx, m.Height()-1)\n\tcase tea.WindowSizeMsg:\n\t\tif m.AutoHeight {\n\t\t\tm.SetHeight(msg.Height - marginBottom)\n\t\t}\n\t\tm.maxIdx = m.Height() - 1\n\tcase tea.KeyPressMsg:\n\t\tswitch {\n\t\tcase key.Matches(msg, m.KeyMap.GoToTop):\n\t\t\tm.selected = 0\n\t\t\tm.minIdx = 0\n\t\t\tm.maxIdx = m.Height() - 1\n\t\tcase key.Matches(msg, m.KeyMap.GoToLast):\n\t\t\tm.selected = len(m.files) - 1\n\t\t\tm.minIdx = len(m.files) - m.Height()\n\t\t\tm.maxIdx = len(m.files) - 1\n\t\tcase key.Matches(msg, m.KeyMap.Down):\n\t\t\tm.selected++\n\t\t\tif m.selected >= len(m.files) {\n\t\t\t\tm.selected = len(m.files) - 1\n\t\t\t}\n\t\t\tif m.selected > m.maxIdx {\n\t\t\t\tm.minIdx++\n\t\t\t\tm.maxIdx++\n\t\t\t}\n\t\tcase key.Matches(msg, m.KeyMap.Up):\n\t\t\tm.selected--\n\t\t\tif m.selected < 0 {\n\t\t\t\tm.selected = 0\n\t\t\t}\n\t\t\tif m.selected < m.minIdx {\n\t\t\t\tm.minIdx--\n\t\t\t\tm.maxIdx--\n\t\t\t}\n\t\tcase key.Matches(msg, m.KeyMap.PageDown):\n\t\t\tm.selected += m.Height()\n\t\t\tif m.selected >= len(m.files) {\n\t\t\t\tm.selected = len(m.files) - 1\n\t\t\t}\n\t\t\tm.minIdx += m.Height()\n\t\t\tm.maxIdx += m.Height()\n\n\t\t\tif m.maxIdx >= len(m.files) {\n\t\t\t\tm.maxIdx = len(m.files) - 1\n\t\t\t\tm.minIdx = m.maxIdx - m.Height()\n\t\t\t}\n\t\tcase key.Matches(msg, m.KeyMap.PageUp):\n\t\t\tm.selected -= m.Height()\n\t\t\tif m.selected < 0 {\n\t\t\t\tm.selected = 0\n\t\t\t}\n\t\t\tm.minIdx -= m.Height()\n\t\t\tm.maxIdx -= m.Height()\n\n\t\t\tif m.minIdx < 0 {\n\t\t\t\tm.minIdx = 0\n\t\t\t\tm.maxIdx = m.minIdx + m.Height()\n\t\t\t}\n\t\tcase key.Matches(msg, m.KeyMap.Back):\n\t\t\tm.CurrentDirectory = filepath.Dir(m.CurrentDirectory)\n\t\t\tif m.selectedStack.Length() > 0 {\n\t\t\t\tm.selected, m.minIdx, m.maxIdx = m.popView()\n\t\t\t} else {\n\t\t\t\tm.selected = 0\n\t\t\t\tm.minIdx = 0\n\t\t\t\tm.maxIdx = m.Height() - 1\n\t\t\t}\n\t\t\treturn m, m.readDir(m.CurrentDirectory, m.ShowHidden)\n\t\tcase key.Matches(msg, m.KeyMap.Open):\n\t\t\tif len(m.files) == 0 {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tf := m.files[m.selected]\n\t\t\tinfo, err := f.Info()\n\t\t\tif err != nil {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tisSymlink := info.Mode()&os.ModeSymlink != 0\n\t\t\tisDir := f.IsDir()\n\n\t\t\tif isSymlink {\n\t\t\t\tsymlinkPath, _ := filepath.EvalSymlinks(filepath.Join(m.CurrentDirectory, f.Name()))\n\t\t\t\tinfo, err := os.Stat(symlinkPath)\n\t\t\t\tif err != nil {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tif info.IsDir() {\n\t\t\t\t\tisDir = true\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (!isDir && m.FileAllowed) || (isDir && m.DirAllowed) {\n\t\t\t\tif key.Matches(msg, m.KeyMap.Select) {\n\t\t\t\t\t// Select the current path as the selection\n\t\t\t\t\tm.Path = filepath.Join(m.CurrentDirectory, f.Name())\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif !isDir {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tm.CurrentDirectory = filepath.Join(m.CurrentDirectory, f.Name())\n\t\t\tm.pushView(m.selected, m.minIdx, m.maxIdx)\n\t\t\tm.selected = 0\n\t\t\tm.minIdx = 0\n\t\t\tm.maxIdx = m.Height() - 1\n\t\t\treturn m, m.readDir(m.CurrentDirectory, m.ShowHidden)\n\t\t}\n\t}\n\treturn m, nil\n}\n\n// View returns the view of the file picker.\nfunc (m Model) View() string {\n\tif len(m.files) == 0 {\n\t\treturn m.Styles.EmptyDirectory.Height(m.Height()).MaxHeight(m.Height()).String()\n\t}\n\tvar s strings.Builder\n\n\tfor i, f := range m.files {\n\t\tif i < m.minIdx || i > m.maxIdx {\n\t\t\tcontinue\n\t\t}\n\n\t\tvar symlinkPath string\n\t\tinfo, err := f.Info()\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tisSymlink := info.Mode()&os.ModeSymlink != 0\n\t\tsize := strings.Replace(humanize.Bytes(uint64(info.Size())), \" \", \"\", 1) //nolint:gosec\n\t\tname := f.Name()\n\n\t\tif isSymlink {\n\t\t\tsymlinkPath, _ = filepath.EvalSymlinks(filepath.Join(m.CurrentDirectory, name))\n\t\t}\n\n\t\tdisabled := !m.canSelect(name) && !f.IsDir()\n\n\t\tif m.selected == i { //nolint:nestif\n\t\t\tselected := \"\"\n\t\t\tif m.ShowPermissions {\n\t\t\t\tselected += \" \" + info.Mode().String()\n\t\t\t}\n\t\t\tif m.ShowSize {\n\t\t\t\tselected += fmt.Sprintf(\"%\"+strconv.Itoa(m.Styles.FileSize.GetWidth())+\"s\", size)\n\t\t\t}\n\t\t\tselected += \" \" + name\n\t\t\tif isSymlink {\n\t\t\t\tselected += \" → \" + symlinkPath\n\t\t\t}\n\t\t\tif disabled {\n\t\t\t\ts.WriteString(m.Styles.DisabledCursor.Render(m.Cursor) + m.Styles.DisabledSelected.Render(selected))\n\t\t\t} else {\n\t\t\t\ts.WriteString(m.Styles.Cursor.Render(m.Cursor) + m.Styles.Selected.Render(selected))\n\t\t\t}\n\t\t\ts.WriteRune('\\n')\n\t\t\tcontinue\n\t\t}\n\n\t\tstyle := m.Styles.File\n\t\tif f.IsDir() {\n\t\t\tstyle = m.Styles.Directory\n\t\t} else if isSymlink {\n\t\t\tstyle = m.Styles.Symlink\n\t\t} else if disabled {\n\t\t\tstyle = m.Styles.DisabledFile\n\t\t}\n\n\t\tfileName := style.Render(name)\n\t\ts.WriteString(m.Styles.Cursor.Render(\" \"))\n\t\tif isSymlink {\n\t\t\tfileName += \" → \" + symlinkPath\n\t\t}\n\t\tif m.ShowPermissions {\n\t\t\ts.WriteString(\" \" + m.Styles.Permission.Render(info.Mode().String()))\n\t\t}\n\t\tif m.ShowSize {\n\t\t\ts.WriteString(m.Styles.FileSize.Render(size))\n\t\t}\n\t\ts.WriteString(\" \" + fileName)\n\t\ts.WriteRune('\\n')\n\t}\n\n\tfor i := lipgloss.Height(s.String()); i <= m.Height(); i++ {\n\t\ts.WriteRune('\\n')\n\t}\n\n\treturn s.String()\n}\n\n// DidSelectFile returns whether a user has selected a file (on this msg).\nfunc (m Model) DidSelectFile(msg tea.Msg) (bool, string) {\n\tdidSelect, path := m.didSelectFile(msg)\n\tif didSelect && m.canSelect(path) {\n\t\treturn true, path\n\t}\n\treturn false, \"\"\n}\n\n// DidSelectDisabledFile returns whether a user tried to select a disabled file\n// (on this msg). This is necessary only if you would like to warn the user that\n// they tried to select a disabled file.\nfunc (m Model) DidSelectDisabledFile(msg tea.Msg) (bool, string) {\n\tdidSelect, path := m.didSelectFile(msg)\n\tif didSelect && !m.canSelect(path) {\n\t\treturn true, path\n\t}\n\treturn false, \"\"\n}\n\nfunc (m Model) didSelectFile(msg tea.Msg) (bool, string) {\n\tif len(m.files) == 0 {\n\t\treturn false, \"\"\n\t}\n\tswitch msg := msg.(type) {\n\tcase tea.KeyPressMsg:\n\t\t// If the msg does not match the Select keymap then this could not have been a selection.\n\t\tif !key.Matches(msg, m.KeyMap.Select) {\n\t\t\treturn false, \"\"\n\t\t}\n\n\t\t// The key press was a selection, let's confirm whether the current file could\n\t\t// be selected or used for navigating deeper into the stack.\n\t\tf := m.files[m.selected]\n\t\tinfo, err := f.Info()\n\t\tif err != nil {\n\t\t\treturn false, \"\"\n\t\t}\n\t\tisSymlink := info.Mode()&os.ModeSymlink != 0\n\t\tisDir := f.IsDir()\n\n\t\tif isSymlink {\n\t\t\tsymlinkPath, _ := filepath.EvalSymlinks(filepath.Join(m.CurrentDirectory, f.Name()))\n\t\t\tinfo, err := os.Stat(symlinkPath)\n\t\t\tif err != nil {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif info.IsDir() {\n\t\t\t\tisDir = true\n\t\t\t}\n\t\t}\n\n\t\tif (!isDir && m.FileAllowed) || (isDir && m.DirAllowed) && m.Path != \"\" {\n\t\t\treturn true, m.Path\n\t\t}\n\n\t\t// If the msg was not a KeyPressMsg, then the file could not have been selected this iteration.\n\t\t// Only a KeyPressMsg can select a file.\n\tdefault:\n\t\treturn false, \"\"\n\t}\n\treturn false, \"\"\n}\n\nfunc (m Model) canSelect(file string) bool {\n\tif len(m.AllowedTypes) <= 0 {\n\t\treturn true\n\t}\n\n\tfor _, ext := range m.AllowedTypes {\n\t\tif strings.HasSuffix(file, ext) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// HighlightedPath returns the path of the currently highlighted file or directory.\nfunc (m Model) HighlightedPath() string {\n\tif len(m.files) == 0 || m.selected < 0 || m.selected >= len(m.files) {\n\t\treturn \"\"\n\t}\n\treturn filepath.Join(m.CurrentDirectory, m.files[m.selected].Name())\n}\n"
  },
  {
    "path": "filepicker/hidden_unix.go",
    "content": "//go:build !windows\n// +build !windows\n\npackage filepicker\n\nimport \"strings\"\n\n// IsHidden reports whether a file is hidden or not.\nfunc IsHidden(file string) (bool, error) {\n\treturn strings.HasPrefix(file, \".\"), nil\n}\n"
  },
  {
    "path": "filepicker/hidden_windows.go",
    "content": "//go:build windows\n// +build windows\n\npackage filepicker\n\nimport (\n\t\"syscall\"\n)\n\n// IsHidden reports whether a file is hidden or not.\nfunc IsHidden(file string) (bool, error) {\n\tpointer, err := syscall.UTF16PtrFromString(file)\n\tif err != nil {\n\t\treturn false, err //nolint:wrapcheck\n\t}\n\tattributes, err := syscall.GetFileAttributes(pointer)\n\tif err != nil {\n\t\treturn false, err //nolint:wrapcheck\n\t}\n\treturn attributes&syscall.FILE_ATTRIBUTE_HIDDEN != 0, nil\n}\n"
  },
  {
    "path": "go.mod",
    "content": "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\tgithub.com/MakeNowJust/heredoc v1.0.0\n\tgithub.com/atotto/clipboard v0.1.4\n\tgithub.com/charmbracelet/harmonica v0.2.0\n\tgithub.com/charmbracelet/x/ansi v0.11.6\n\tgithub.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f\n\tgithub.com/dustin/go-humanize v1.0.1\n\tgithub.com/mattn/go-runewidth v0.0.21\n\tgithub.com/rivo/uniseg v0.4.7\n\tgithub.com/sahilm/fuzzy v0.1.1\n)\n\nrequire (\n\tgithub.com/aymanbagabas/go-udiff v0.4.1 // indirect\n\tgithub.com/charmbracelet/colorprofile v0.4.2 // indirect\n\tgithub.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect\n\tgithub.com/charmbracelet/x/term v0.2.2 // indirect\n\tgithub.com/charmbracelet/x/termios v0.1.1 // indirect\n\tgithub.com/charmbracelet/x/windows v0.2.2 // indirect\n\tgithub.com/clipperhouse/displaywidth v0.11.0 // indirect\n\tgithub.com/clipperhouse/uax29/v2 v2.7.0 // indirect\n\tgithub.com/kylelemons/godebug v1.1.0 // indirect\n\tgithub.com/lucasb-eyer/go-colorful v1.3.0 // indirect\n\tgithub.com/muesli/cancelreader v0.2.2 // indirect\n\tgithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect\n\tgolang.org/x/sync v0.19.0 // indirect\n\tgolang.org/x/sys v0.42.0 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "charm.land/bubbletea/v2 v2.0.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0=\ncharm.land/bubbletea/v2 v2.0.2/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ=\ncharm.land/lipgloss/v2 v2.0.2 h1:xFolbF8JdpNkM2cEPTfXEcW1p6NRzOWTSamRfYEw8cs=\ncharm.land/lipgloss/v2 v2.0.2/go.mod h1:KjPle2Qd3YmvP1KL5OMHiHysGcNwq6u83MUjYkFvEkM=\ngithub.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=\ngithub.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=\ngithub.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=\ngithub.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=\ngithub.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=\ngithub.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=\ngithub.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY=\ngithub.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8=\ngithub.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=\ngithub.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=\ngithub.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 h1:eyFRbAmexyt43hVfeyBofiGSEmJ7krjLOYt/9CF5NKA=\ngithub.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8/go.mod h1:SQpCTRNBtzJkwku5ye4S3HEuthAlGy2n9VXZnWkEW98=\ngithub.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=\ngithub.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=\ngithub.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=\ngithub.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=\ngithub.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=\ngithub.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=\ngithub.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=\ngithub.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=\ngithub.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=\ngithub.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=\ngithub.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=\ngithub.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=\ngithub.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=\ngithub.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=\ngithub.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=\ngithub.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=\ngithub.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=\ngithub.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=\ngithub.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=\ngithub.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=\ngithub.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w=\ngithub.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=\ngithub.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=\ngithub.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=\ngithub.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=\ngithub.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=\ngithub.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=\ngithub.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=\ngithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=\ngithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=\ngolang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=\ngolang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=\ngolang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=\ngolang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=\ngolang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=\ngolang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\n"
  },
  {
    "path": "help/help.go",
    "content": "// Package help provides a simple help view for Bubble Tea applications.\npackage help\n\nimport (\n\t\"strings\"\n\n\t\"charm.land/bubbles/v2/key\"\n\ttea \"charm.land/bubbletea/v2\"\n\t\"charm.land/lipgloss/v2\"\n)\n\n// KeyMap is a map of keybindings used to generate help. Since it's an\n// interface it can be any type, though struct or a map[string][]key.Binding\n// are likely candidates.\n//\n// Note that if a key is disabled (via key.Binding.SetEnabled) it will not be\n// rendered in the help view, so in theory generated help should self-manage.\ntype KeyMap interface {\n\t// ShortHelp returns a slice of bindings to be displayed in the short\n\t// version of the help. The help bubble will render help in the order in\n\t// which the help items are returned here.\n\tShortHelp() []key.Binding\n\n\t// FullHelp returns an extended group of help items, grouped by columns.\n\t// The help bubble will render the help in the order in which the help\n\t// items are returned here.\n\tFullHelp() [][]key.Binding\n}\n\n// Styles is a set of available style definitions for the Help bubble.\ntype Styles struct {\n\tEllipsis lipgloss.Style\n\n\t// Styling for the short help\n\tShortKey       lipgloss.Style\n\tShortDesc      lipgloss.Style\n\tShortSeparator lipgloss.Style\n\n\t// Styling for the full help\n\tFullKey       lipgloss.Style\n\tFullDesc      lipgloss.Style\n\tFullSeparator lipgloss.Style\n}\n\n// DefaultStyles returns a set of default styles for the help bubble. Light or\n// dark styles can be selected by passing true or false to the isDark\n// parameter.\nfunc DefaultStyles(isDark bool) Styles {\n\tlightDark := lipgloss.LightDark(isDark)\n\n\tkeyStyle := lipgloss.NewStyle().Foreground(lightDark(lipgloss.Color(\"#909090\"), lipgloss.Color(\"#626262\")))\n\tdescStyle := lipgloss.NewStyle().Foreground(lightDark(lipgloss.Color(\"#B2B2B2\"), lipgloss.Color(\"#4A4A4A\")))\n\tsepStyle := lipgloss.NewStyle().Foreground(lightDark(lipgloss.Color(\"#DADADA\"), lipgloss.Color(\"#3C3C3C\")))\n\n\treturn Styles{\n\t\tShortKey:       keyStyle,\n\t\tShortDesc:      descStyle,\n\t\tShortSeparator: sepStyle,\n\t\tEllipsis:       sepStyle,\n\t\tFullKey:        keyStyle,\n\t\tFullDesc:       descStyle,\n\t\tFullSeparator:  sepStyle,\n\t}\n}\n\n// DefaultDarkStyles returns a set of default styles for dark backgrounds.\nfunc DefaultDarkStyles() Styles {\n\treturn DefaultStyles(true)\n}\n\n// DefaultLightStyles returns a set of default styles for light backgrounds.\nfunc DefaultLightStyles() Styles {\n\treturn DefaultStyles(false)\n}\n\n// Model contains the state of the help view.\ntype Model struct {\n\tShowAll bool // if true, render the \"full\" help menu\n\n\tShortSeparator string\n\tFullSeparator  string\n\n\t// The symbol we use in the short help when help items have been truncated\n\t// due to width. Periods of ellipsis by default.\n\tEllipsis string\n\n\tStyles Styles\n\n\twidth int\n}\n\n// New creates a new help view with some useful defaults.\nfunc New() Model {\n\treturn Model{\n\t\tShortSeparator: \" • \",\n\t\tFullSeparator:  \"    \",\n\t\tEllipsis:       \"…\",\n\t\tStyles:         DefaultDarkStyles(),\n\t}\n}\n\n// Update helps satisfy the Bubble Tea Model interface. It's a no-op.\nfunc (m Model) Update(_ tea.Msg) (Model, tea.Cmd) {\n\treturn m, nil\n}\n\n// View renders the help view's current state.\nfunc (m Model) View(k KeyMap) string {\n\tif m.ShowAll {\n\t\treturn m.FullHelpView(k.FullHelp())\n\t}\n\treturn m.ShortHelpView(k.ShortHelp())\n}\n\n// SetWidth sets the maximum width for the help view.\nfunc (m *Model) SetWidth(w int) {\n\tm.width = w\n}\n\n// Width returns the maximum width for the help view.\nfunc (m Model) Width() int {\n\treturn m.width\n}\n\n// ShortHelpView renders a single line help view from a slice of keybindings.\n// If the line is longer than the maximum width it will be gracefully\n// truncated, showing only as many help items as possible.\nfunc (m Model) ShortHelpView(bindings []key.Binding) string {\n\tif len(bindings) == 0 {\n\t\treturn \"\"\n\t}\n\n\tvar b strings.Builder\n\tvar totalWidth int\n\tseparator := m.Styles.ShortSeparator.Inline(true).Render(m.ShortSeparator)\n\n\tfor i, kb := range bindings {\n\t\tif !kb.Enabled() {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Sep\n\t\tvar sep string\n\t\tif totalWidth > 0 && i < len(bindings) {\n\t\t\tsep = separator\n\t\t}\n\n\t\t// Item\n\t\tstr := sep +\n\t\t\tm.Styles.ShortKey.Inline(true).Render(kb.Help().Key) + \" \" +\n\t\t\tm.Styles.ShortDesc.Inline(true).Render(kb.Help().Desc)\n\t\tw := lipgloss.Width(str)\n\n\t\t// Tail\n\t\tif tail, ok := m.shouldAddItem(totalWidth, w); !ok {\n\t\t\tif tail != \"\" {\n\t\t\t\tb.WriteString(tail)\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\n\t\ttotalWidth += w\n\t\tb.WriteString(str)\n\t}\n\n\treturn b.String()\n}\n\n// FullHelpView renders help columns from a slice of key binding slices. Each\n// top level slice entry renders into a column.\nfunc (m Model) FullHelpView(groups [][]key.Binding) string {\n\tif len(groups) == 0 {\n\t\treturn \"\"\n\t}\n\n\t// Linter note: at this time we don't think it's worth the additional\n\t// code complexity involved in preallocating this slice.\n\tvar (\n\t\tout []string\n\n\t\ttotalWidth int\n\t\tseparator  = m.Styles.FullSeparator.Inline(true).Render(m.FullSeparator)\n\t)\n\n\t// Iterate over groups to build columns\n\tfor i, group := range groups {\n\t\tif group == nil || !shouldRenderColumn(group) {\n\t\t\tcontinue\n\t\t}\n\t\tvar (\n\t\t\tsep          string\n\t\t\tkeys         []string\n\t\t\tdescriptions []string\n\t\t)\n\n\t\t// Sep\n\t\tif totalWidth > 0 && i < len(groups) {\n\t\t\tsep = separator\n\t\t}\n\n\t\t// Separate keys and descriptions into different slices\n\t\tfor _, kb := range group {\n\t\t\tif !kb.Enabled() {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tkeys = append(keys, kb.Help().Key)\n\t\t\tdescriptions = append(descriptions, kb.Help().Desc)\n\t\t}\n\n\t\t// Column\n\t\tcol := lipgloss.JoinHorizontal(lipgloss.Top,\n\t\t\tsep,\n\t\t\tm.Styles.FullKey.Render(strings.Join(keys, \"\\n\")),\n\t\t\t\" \",\n\t\t\tm.Styles.FullDesc.Render(strings.Join(descriptions, \"\\n\")),\n\t\t)\n\t\tw := lipgloss.Width(col)\n\n\t\t// Tail\n\t\tif tail, ok := m.shouldAddItem(totalWidth, w); !ok {\n\t\t\tif tail != \"\" {\n\t\t\t\tout = append(out, tail)\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\n\t\ttotalWidth += w\n\t\tout = append(out, col)\n\t}\n\n\treturn lipgloss.JoinHorizontal(lipgloss.Top, out...)\n}\n\nfunc (m Model) shouldAddItem(totalWidth, width int) (tail string, ok bool) {\n\t// If there's room for an ellipsis, print that.\n\tif m.width > 0 && totalWidth+width > m.width {\n\t\ttail = \" \" + m.Styles.Ellipsis.Inline(true).Render(m.Ellipsis)\n\n\t\tif totalWidth+lipgloss.Width(tail) < m.width {\n\t\t\treturn tail, false\n\t\t}\n\t}\n\treturn \"\", true\n}\n\nfunc shouldRenderColumn(b []key.Binding) (ok bool) {\n\tfor _, v := range b {\n\t\tif v.Enabled() {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "help/help_test.go",
    "content": "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/charmbracelet/x/exp/golden\"\n)\n\nfunc TestFullHelp(t *testing.T) {\n\tm := New()\n\tm.FullSeparator = \" | \"\n\tk := key.WithKeys(\"x\")\n\tkb := [][]key.Binding{\n\t\t{\n\t\t\tkey.NewBinding(k, key.WithHelp(\"enter\", \"continue\")),\n\t\t},\n\t\t{\n\t\t\tkey.NewBinding(k, key.WithHelp(\"esc\", \"back\")),\n\t\t\tkey.NewBinding(k, key.WithHelp(\"?\", \"help\")),\n\t\t},\n\t\t{\n\t\t\tkey.NewBinding(k, key.WithHelp(\"H\", \"home\")),\n\t\t\tkey.NewBinding(k, key.WithHelp(\"ctrl+c\", \"quit\")),\n\t\t\tkey.NewBinding(k, key.WithHelp(\"ctrl+l\", \"log\")),\n\t\t},\n\t}\n\n\tfor _, w := range []int{20, 30, 40} {\n\t\tt.Run(fmt.Sprintf(\"full help %d width\", w), func(t *testing.T) {\n\t\t\tm.SetWidth(w)\n\t\t\ts := m.FullHelpView(kb)\n\t\t\ts = ansi.Strip(s)\n\t\t\tgolden.RequireEqual(t, []byte(s))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "help/testdata/TestFullHelp/full_help_20_width.golden",
    "content": "enter continue …"
  },
  {
    "path": "help/testdata/TestFullHelp/full_help_30_width.golden",
    "content": "enter continue | esc back …\n                 ?   help  "
  },
  {
    "path": "help/testdata/TestFullHelp/full_help_40_width.golden",
    "content": "enter continue | esc back | H      home\n                 ?   help   ctrl+c quit\n                            ctrl+l log "
  },
  {
    "path": "internal/memoization/memoization.go",
    "content": "// Package memoization implement a simple memoization cache. It's designed to\n// improve performance in textarea.\npackage memoization\n\nimport (\n\t\"container/list\"\n\t\"crypto/sha256\"\n\t\"fmt\"\n\t\"sync\"\n)\n\n// Hasher is an interface that requires a Hash method. The Hash method is\n// expected to return a string representation of the hash of the object.\ntype Hasher interface {\n\tHash() string\n}\n\n// entry is a struct that holds a key-value pair. It is used as an element\n// in the evictionList of the MemoCache.\ntype entry[T any] struct {\n\tkey   string\n\tvalue T\n}\n\n// MemoCache is a struct that represents a cache with a set capacity. It\n// uses an LRU (Least Recently Used) eviction policy. It is safe for\n// concurrent use.\ntype MemoCache[H Hasher, T any] struct {\n\tcapacity      int\n\tmutex         sync.Mutex\n\tcache         map[string]*list.Element // The cache holding the results\n\tevictionList  *list.List               // A list to keep track of the order for LRU\n\thashableItems map[string]T             // This map keeps track of the original hashable items (optional)\n}\n\n// NewMemoCache is a function that creates a new MemoCache with a given\n// capacity. It returns a pointer to the created MemoCache.\nfunc NewMemoCache[H Hasher, T any](capacity int) *MemoCache[H, T] {\n\treturn &MemoCache[H, T]{\n\t\tcapacity:      capacity,\n\t\tcache:         make(map[string]*list.Element),\n\t\tevictionList:  list.New(),\n\t\thashableItems: make(map[string]T),\n\t}\n}\n\n// Capacity is a method that returns the capacity of the MemoCache.\nfunc (m *MemoCache[H, T]) Capacity() int {\n\treturn m.capacity\n}\n\n// Size is a method that returns the current size of the MemoCache. It is\n// the number of items currently stored in the cache.\nfunc (m *MemoCache[H, T]) Size() int {\n\tm.mutex.Lock()\n\tdefer m.mutex.Unlock()\n\treturn m.evictionList.Len()\n}\n\n// Get is a method that returns the value associated with the given\n// hashable item in the MemoCache. If there is no corresponding value, the\n// method returns nil.\nfunc (m *MemoCache[H, T]) Get(h H) (T, bool) {\n\tm.mutex.Lock()\n\tdefer m.mutex.Unlock()\n\n\thashedKey := h.Hash()\n\tif element, found := m.cache[hashedKey]; found {\n\t\tm.evictionList.MoveToFront(element)\n\t\treturn element.Value.(*entry[T]).value, true\n\t}\n\tvar result T\n\treturn result, false\n}\n\n// Set is a method that sets the value for the given hashable item in the\n// MemoCache. If the cache is at capacity, it evicts the least recently\n// used item before adding the new item.\nfunc (m *MemoCache[H, T]) Set(h H, value T) {\n\tm.mutex.Lock()\n\tdefer m.mutex.Unlock()\n\n\thashedKey := h.Hash()\n\tif element, found := m.cache[hashedKey]; found {\n\t\tm.evictionList.MoveToFront(element)\n\t\telement.Value.(*entry[T]).value = value\n\t\treturn\n\t}\n\n\t// Check if the cache is at capacity\n\tif m.evictionList.Len() >= m.capacity {\n\t\t// Evict the least recently used item from the cache\n\t\ttoEvict := m.evictionList.Back()\n\t\tif toEvict != nil {\n\t\t\tevictedEntry := m.evictionList.Remove(toEvict).(*entry[T])\n\t\t\tdelete(m.cache, evictedEntry.key)\n\t\t\tdelete(m.hashableItems, evictedEntry.key) // if you're keeping track of original items\n\t\t}\n\t}\n\n\t// Add the value to the cache and the evictionList\n\tnewEntry := &entry[T]{\n\t\tkey:   hashedKey,\n\t\tvalue: value,\n\t}\n\telement := m.evictionList.PushFront(newEntry)\n\tm.cache[hashedKey] = element\n\tm.hashableItems[hashedKey] = value // if you're keeping track of original items\n}\n\n// HString is a type that implements the Hasher interface for strings.\ntype HString string\n\n// Hash is a method that returns the hash of the string.\nfunc (h HString) Hash() string {\n\treturn fmt.Sprintf(\"%x\", sha256.Sum256([]byte(h)))\n}\n\n// HInt is a type that implements the Hasher interface for integers.\ntype HInt int\n\n// Hash is a method that returns the hash of the integer.\nfunc (h HInt) Hash() string {\n\treturn fmt.Sprintf(\"%x\", sha256.Sum256(fmt.Appendf(nil, \"%d\", h)))\n}\n"
  },
  {
    "path": "internal/memoization/memoization_test.go",
    "content": "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 actionType = iota\n\tget\n)\n\ntype cacheAction struct {\n\tactionType    actionType\n\tkey           HString\n\tvalue         any\n\texpectedValue any\n}\n\ntype testCase struct {\n\tname     string\n\tcapacity int\n\tactions  []cacheAction\n}\n\nfunc TestCache(t *testing.T) {\n\ttests := []testCase{\n\t\t{\n\t\t\tname:     \"TestNewMemoCache\",\n\t\t\tcapacity: 5,\n\t\t\tactions: []cacheAction{\n\t\t\t\t{actionType: get, expectedValue: nil},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"TestSetAndGet\",\n\t\t\tcapacity: 10,\n\t\t\tactions: []cacheAction{\n\t\t\t\t{actionType: set, key: \"key1\", value: \"value1\"},\n\t\t\t\t{actionType: get, key: \"key1\", expectedValue: \"value1\"},\n\t\t\t\t{actionType: set, key: \"key1\", value: \"newValue1\"},\n\t\t\t\t{actionType: get, key: \"key1\", expectedValue: \"newValue1\"},\n\t\t\t\t{actionType: get, key: \"nonExistentKey\", expectedValue: nil},\n\t\t\t\t{actionType: set, key: \"nilKey\", value: \"\"},\n\t\t\t\t{actionType: get, key: \"nilKey\", expectedValue: \"\"},\n\t\t\t\t{actionType: set, key: \"keyA\", value: \"valueA\"},\n\t\t\t\t{actionType: set, key: \"keyB\", value: \"valueB\"},\n\t\t\t\t{actionType: get, key: \"keyA\", expectedValue: \"valueA\"},\n\t\t\t\t{actionType: get, key: \"keyB\", expectedValue: \"valueB\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"TestSetNilValue\",\n\t\t\tcapacity: 10,\n\t\t\tactions: []cacheAction{\n\t\t\t\t{actionType: set, key: HString(\"nilKey\"), value: nil},\n\t\t\t\t{actionType: get, key: HString(\"nilKey\"), expectedValue: nil},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"TestGetAfterEviction\",\n\t\t\tcapacity: 2,\n\t\t\tactions: []cacheAction{\n\t\t\t\t{actionType: set, key: HString(\"1\"), value: 1},\n\t\t\t\t{actionType: set, key: HString(\"2\"), value: 2},\n\t\t\t\t{actionType: set, key: HString(\"3\"), value: 3},\n\t\t\t\t{actionType: get, key: HString(\"1\"), expectedValue: nil},\n\t\t\t\t{actionType: get, key: HString(\"2\"), expectedValue: 2},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"TestGetAfterLRU\",\n\t\t\tcapacity: 2,\n\t\t\tactions: []cacheAction{\n\t\t\t\t{actionType: set, key: HString(\"1\"), value: 1},\n\t\t\t\t{actionType: set, key: HString(\"2\"), value: 2},\n\t\t\t\t{actionType: get, key: HString(\"1\"), expectedValue: 1},\n\t\t\t\t{actionType: set, key: HString(\"3\"), value: 3},\n\t\t\t\t{actionType: get, key: HString(\"1\"), expectedValue: 1},\n\t\t\t\t{actionType: get, key: HString(\"3\"), expectedValue: 3},\n\t\t\t\t{actionType: get, key: HString(\"2\"), expectedValue: nil},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"TestLRU_Capacity3\",\n\t\t\tcapacity: 3,\n\t\t\tactions: []cacheAction{\n\t\t\t\t{actionType: set, key: HString(\"1\"), value: 1},\n\t\t\t\t{actionType: set, key: HString(\"2\"), value: 2},\n\t\t\t\t{actionType: set, key: HString(\"3\"), value: 3},\n\t\t\t\t{actionType: get, key: HString(\"1\"), expectedValue: 1}, // Accessing key \"1\"\n\t\t\t\t{actionType: set, key: HString(\"4\"), value: 4},         // Should evict key \"2\" since \"1\" was recently accessed\n\t\t\t\t{actionType: get, key: HString(\"2\"), expectedValue: nil},\n\t\t\t\t{actionType: get, key: HString(\"1\"), expectedValue: 1},\n\t\t\t\t{actionType: get, key: HString(\"3\"), expectedValue: 3},\n\t\t\t\t{actionType: get, key: HString(\"4\"), expectedValue: 4},\n\t\t\t},\n\t\t},\n\t\t// Test LRU behavior with varying accesses\n\t\t{\n\t\t\tname:     \"TestLRU_VaryingAccesses\",\n\t\t\tcapacity: 3,\n\t\t\tactions: []cacheAction{\n\t\t\t\t{actionType: set, key: HString(\"1\"), value: 1},\n\t\t\t\t{actionType: set, key: HString(\"2\"), value: 2},\n\t\t\t\t{actionType: set, key: HString(\"3\"), value: 3},\n\t\t\t\t{actionType: get, key: HString(\"1\"), expectedValue: 1}, // Accessing key \"1\"\n\t\t\t\t{actionType: get, key: HString(\"2\"), expectedValue: 2}, // Accessing key \"2\"\n\t\t\t\t{actionType: set, key: HString(\"4\"), value: 4},         // Should evict key \"3\"\n\t\t\t\t{actionType: get, key: HString(\"3\"), expectedValue: nil},\n\t\t\t\t{actionType: get, key: HString(\"1\"), expectedValue: 1},\n\t\t\t\t{actionType: get, key: HString(\"2\"), expectedValue: 2},\n\t\t\t\t{actionType: get, key: HString(\"4\"), expectedValue: 4},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tcache := NewMemoCache[HString, any](tt.capacity)\n\t\t\tfor _, action := range tt.actions {\n\t\t\t\tswitch action.actionType {\n\t\t\t\tcase set:\n\t\t\t\t\tcache.Set(action.key, action.value)\n\t\t\t\tcase get:\n\t\t\t\t\tif got, _ := cache.Get(action.key); got != action.expectedValue {\n\t\t\t\t\t\tt.Errorf(\"Get() = %v, want %v\", got, action.expectedValue)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc FuzzCache(f *testing.F) {\n\t// Define some seed values for initial scenarios\n\tfor _, seed := range [][]byte{\n\t\t[]byte(\"7\\x010\\x0000000020\"),\n\t\t{0, 0, 0, 0}, // Set key 0 to 0\n\t\t{1, 0, 0, 1}, // Set key 0 to 1\n\t\t{2, 0},       // Get key 0\n\t} {\n\t\tf.Add(seed)\n\t}\n\n\tf.Fuzz(func(t *testing.T, in []byte) {\n\t\tif len(in) < 1 {\n\t\t\tt.Skip() // Skip the test if the input is less than 1 byte\n\t\t}\n\n\t\tcache := NewMemoCache[HInt, int](10) // Initialize a cache with the initial size\n\n\t\texpectedValues := make(map[HInt]int) // Map to store expected key-value pairs\n\t\taccessOrder := make([]HInt, 0)       // Slice to store the order of keys accessed\n\n\t\tfor i := 0; i < len(in); {\n\t\t\topCode := in[i] % 4 // Determine the operation: Set, Get, or Reset (added case for Reset)\n\t\t\ti++\n\n\t\t\tswitch opCode {\n\t\t\tcase 0, 1: // Set operation\n\t\t\t\tif i+3 > len(in) {\n\t\t\t\t\tt.Skip() // Not enough input to continue, so skip\n\t\t\t\t}\n\n\t\t\t\tkey := HInt(binary.BigEndian.Uint16(in[i : i+2]))\n\t\t\t\tvalue := int(in[i+2])\n\t\t\t\ti += 3\n\n\t\t\t\t// If the key is already in accessOrder, we remove it and append it again later\n\t\t\t\tfor index, accessedKey := range accessOrder {\n\t\t\t\t\tif accessedKey == key {\n\t\t\t\t\t\taccessOrder = slices.Delete(accessOrder, index, index+1)\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tcache.Set(key, value) // Set the value in the cache\n\t\t\t\texpectedValues[key] = value\n\t\t\t\taccessOrder = append(accessOrder, key) // Add the key to the access order slice\n\n\t\t\t\t// If we exceeded the cache size, we need to evict the least recently used item\n\t\t\t\tif len(accessOrder) > cache.Capacity() {\n\t\t\t\t\tevictedKey := accessOrder[0]\n\t\t\t\t\taccessOrder = accessOrder[1:]\n\t\t\t\t\tdelete(expectedValues, evictedKey) // Remove the evicted key from expected values\n\t\t\t\t}\n\n\t\t\tcase 2: // Get operation\n\t\t\t\tif i >= len(in) {\n\t\t\t\t\tt.Skip() // Not enough input to continue, so skip\n\t\t\t\t}\n\n\t\t\t\tkey := HInt(in[i])\n\t\t\t\ti++\n\n\t\t\t\texpectedValue, ok := expectedValues[key]\n\t\t\t\tif !ok {\n\t\t\t\t\t// If the key is not found, it means it was either evicted or never added\n\t\t\t\t\texpectedValue = 0 // The zero value, depends on your cache implementation\n\t\t\t\t} else {\n\t\t\t\t\t// If the key was accessed, move it to the end of the accessOrder to represent recent use\n\t\t\t\t\tfor index, accessedKey := range accessOrder {\n\t\t\t\t\t\tif accessedKey == key {\n\t\t\t\t\t\t\taccessOrder = slices.Delete(accessOrder, index, index+1)\n\t\t\t\t\t\t\taccessOrder = append(accessOrder, key)\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif got, _ := cache.Get(key); got != expectedValue {\n\t\t\t\t\tfmt.Fprintf(os.Stderr, \"cache: capacity: %d, hashable: %v, cache: %v\\n\", cache.capacity, cache.hashableItems, cache.cache)\n\t\t\t\t\tt.Fatalf(\"Get(%v) = %v, want %v\", key, got, expectedValue) // The values do not match\n\t\t\t\t}\n\t\t\tcase 3: // Reset operation\n\t\t\t\tif i >= len(in) {\n\t\t\t\t\tt.Skip() // Not enough input to continue, so skip\n\t\t\t\t}\n\n\t\t\t\tnewCacheSize := int(in[i]) // Read the new cache size from the input\n\t\t\t\ti++\n\n\t\t\t\tif newCacheSize == 0 {\n\t\t\t\t\tt.Skip() // If the size is zero, we skip this test\n\t\t\t\t}\n\n\t\t\t\t// Create a new cache with the specified size\n\t\t\t\tcache = NewMemoCache[HInt, int](newCacheSize)\n\n\t\t\t\t// clear and reinitialize the expected values\n\t\t\t\texpectedValues = make(map[HInt]int)\n\t\t\t\taccessOrder = make([]HInt, 0)\n\t\t\t}\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "internal/runeutil/runeutil.go",
    "content": "// Package runeutil provides utility functions for tidying up incoming runes\n// from Key messages.\npackage runeutil\n\nimport (\n\t\"unicode\"\n\t\"unicode/utf8\"\n)\n\n// Sanitizer is a helper for bubble widgets that want to process\n// Runes from input key messages.\ntype Sanitizer interface {\n\t// Sanitize removes control characters from runes in a KeyRunes\n\t// message, and optionally replaces newline/carriage return/tabs by a\n\t// specified character.\n\t//\n\t// The rune array is modified in-place if possible. In that case, the\n\t// returned slice is the original slice shortened after the control\n\t// characters have been removed/translated.\n\tSanitize(runes []rune) []rune\n}\n\n// NewSanitizer constructs a rune sanitizer.\nfunc NewSanitizer(opts ...Option) Sanitizer {\n\ts := sanitizer{\n\t\treplaceNewLine: []rune(\"\\n\"),\n\t\treplaceTab:     []rune(\"    \"),\n\t}\n\tfor _, o := range opts {\n\t\ts = o(s)\n\t}\n\treturn &s\n}\n\n// Option is the type of option that can be passed to Sanitize().\ntype Option func(sanitizer) sanitizer\n\n// ReplaceTabs replaces tabs by the specified string.\nfunc ReplaceTabs(tabRepl string) Option {\n\treturn func(s sanitizer) sanitizer {\n\t\ts.replaceTab = []rune(tabRepl)\n\t\treturn s\n\t}\n}\n\n// ReplaceNewlines replaces newline characters by the specified string.\nfunc ReplaceNewlines(nlRepl string) Option {\n\treturn func(s sanitizer) sanitizer {\n\t\ts.replaceNewLine = []rune(nlRepl)\n\t\treturn s\n\t}\n}\n\nfunc (s *sanitizer) Sanitize(runes []rune) []rune {\n\t// dstrunes are where we are storing the result.\n\tdstrunes := runes[:0:len(runes)]\n\t// copied indicates whether dstrunes is an alias of runes\n\t// or a copy. We need a copy when dst moves past src.\n\t// We use this as an optimization to avoid allocating\n\t// a new rune slice in the common case where the output\n\t// is smaller or equal to the input.\n\tcopied := false\n\n\tfor src := range runes {\n\t\tr := runes[src]\n\t\tswitch {\n\t\tcase r == utf8.RuneError:\n\t\t\t// skip\n\n\t\tcase r == '\\r' || r == '\\n':\n\t\t\tif len(dstrunes)+len(s.replaceNewLine) > src && !copied {\n\t\t\t\tdst := len(dstrunes)\n\t\t\t\tdstrunes = make([]rune, dst, len(runes)+len(s.replaceNewLine))\n\t\t\t\tcopy(dstrunes, runes[:dst])\n\t\t\t\tcopied = true\n\t\t\t}\n\t\t\tdstrunes = append(dstrunes, s.replaceNewLine...)\n\n\t\tcase r == '\\t':\n\t\t\tif len(dstrunes)+len(s.replaceTab) > src && !copied {\n\t\t\t\tdst := len(dstrunes)\n\t\t\t\tdstrunes = make([]rune, dst, len(runes)+len(s.replaceTab))\n\t\t\t\tcopy(dstrunes, runes[:dst])\n\t\t\t\tcopied = true\n\t\t\t}\n\t\t\tdstrunes = append(dstrunes, s.replaceTab...)\n\n\t\tcase unicode.IsControl(r):\n\t\t\t// Other control characters: skip.\n\n\t\tdefault:\n\t\t\t// Keep the character.\n\t\t\tdstrunes = append(dstrunes, runes[src])\n\t\t}\n\t}\n\treturn dstrunes\n}\n\ntype sanitizer struct {\n\treplaceNewLine []rune\n\treplaceTab     []rune\n}\n"
  },
  {
    "path": "internal/runeutil/runeutil_test.go",
    "content": "package runeutil\n\nimport (\n\t\"testing\"\n\t\"unicode/utf8\"\n)\n\nfunc TestSanitize(t *testing.T) {\n\ttd := []struct {\n\t\tinput, output string\n\t}{\n\t\t{\"\", \"\"},\n\t\t{\"x\", \"x\"},\n\t\t{\"\\n\", \"XX\"},\n\t\t{\"\\na\\n\", \"XXaXX\"},\n\t\t{\"\\n\\n\", \"XXXX\"},\n\t\t{\"\\t\", \"\"},\n\t\t{\"hello\", \"hello\"},\n\t\t{\"hel\\nlo\", \"helXXlo\"},\n\t\t{\"hel\\rlo\", \"helXXlo\"},\n\t\t{\"hel\\tlo\", \"hello\"},\n\t\t{\"he\\n\\nl\\tlo\", \"heXXXXllo\"},\n\t\t{\"he\\tl\\n\\nlo\", \"helXXXXlo\"},\n\t\t{\"hel\\x1blo\", \"hello\"},\n\t\t{\"hello\\xc2\", \"hello\"}, // invalid utf8\n\t}\n\n\tfor _, tc := range td {\n\t\trunes := make([]rune, 0, len(tc.input))\n\t\tb := []byte(tc.input)\n\t\tfor i, w := 0, 0; i < len(b); i += w {\n\t\t\tvar r rune\n\t\t\tr, w = utf8.DecodeRune(b[i:])\n\t\t\trunes = append(runes, r)\n\t\t}\n\t\tt.Logf(\"input runes: %+v\", runes)\n\t\ts := NewSanitizer(ReplaceNewlines(\"XX\"), ReplaceTabs(\"\"))\n\t\tresult := s.Sanitize(runes)\n\t\trs := string(result)\n\t\tif tc.output != rs {\n\t\t\tt.Errorf(\"%q: expected %q, got %q (%+v)\", tc.input, tc.output, rs, result)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "key/key.go",
    "content": "// Package key provides some types and functions for generating user-definable\n// keymappings useful in Bubble Tea components. There are a few different ways\n// you can define a keymapping with this package. Here's one example:\n//\n//\ttype KeyMap struct {\n//\t    Up key.Binding\n//\t    Down key.Binding\n//\t}\n//\n//\tvar DefaultKeyMap = KeyMap{\n//\t    Up: key.NewBinding(\n//\t        key.WithKeys(\"k\", \"up\"),        // actual keybindings\n//\t        key.WithHelp(\"↑/k\", \"move up\"), // corresponding help text\n//\t    ),\n//\t    Down: key.NewBinding(\n//\t        key.WithKeys(\"j\", \"down\"),\n//\t        key.WithHelp(\"↓/j\", \"move down\"),\n//\t    ),\n//\t}\n//\n//\tfunc (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {\n//\t    switch msg := msg.(type) {\n//\t    case tea.KeyPressMsg:\n//\t        switch {\n//\t        case key.Matches(msg, DefaultKeyMap.Up):\n//\t            // The user pressed up\n//\t        case key.Matches(msg, DefaultKeyMap.Down):\n//\t            // The user pressed down\n//\t        }\n//\t    }\n//\n//\t    // ...\n//\t}\n//\n// The help information, which is not used in the example above, can be used\n// to render help text for keystrokes in your views.\npackage key\n\nimport \"fmt\"\n\n// Binding describes a set of keybindings and, optionally, their associated\n// help text.\ntype Binding struct {\n\tkeys     []string\n\thelp     Help\n\tdisabled bool\n}\n\n// BindingOpt is an initialization option for a keybinding. It's used as an\n// argument to NewBinding.\ntype BindingOpt func(*Binding)\n\n// NewBinding returns a new keybinding from a set of BindingOpt options.\nfunc NewBinding(opts ...BindingOpt) Binding {\n\tb := &Binding{}\n\tfor _, opt := range opts {\n\t\topt(b)\n\t}\n\treturn *b\n}\n\n// WithKeys initializes a keybinding with the given keystrokes.\nfunc WithKeys(keys ...string) BindingOpt {\n\treturn func(b *Binding) {\n\t\tb.keys = keys\n\t}\n}\n\n// WithHelp initializes a keybinding with the given help text.\nfunc WithHelp(key, desc string) BindingOpt {\n\treturn func(b *Binding) {\n\t\tb.help = Help{Key: key, Desc: desc}\n\t}\n}\n\n// WithDisabled initializes a disabled keybinding.\nfunc WithDisabled() BindingOpt {\n\treturn func(b *Binding) {\n\t\tb.disabled = true\n\t}\n}\n\n// SetKeys sets the keys for the keybinding.\nfunc (b *Binding) SetKeys(keys ...string) {\n\tb.keys = keys\n}\n\n// Keys returns the keys for the keybinding.\nfunc (b Binding) Keys() []string {\n\treturn b.keys\n}\n\n// SetHelp sets the help text for the keybinding.\nfunc (b *Binding) SetHelp(key, desc string) {\n\tb.help = Help{Key: key, Desc: desc}\n}\n\n// Help returns the Help information for the keybinding.\nfunc (b Binding) Help() Help {\n\treturn b.help\n}\n\n// Enabled returns whether or not the keybinding is enabled. Disabled\n// keybindings won't be activated and won't show up in help. Keybindings are\n// enabled by default.\nfunc (b Binding) Enabled() bool {\n\treturn !b.disabled && b.keys != nil\n}\n\n// SetEnabled enables or disables the keybinding.\nfunc (b *Binding) SetEnabled(v bool) {\n\tb.disabled = !v\n}\n\n// Unbind removes the keys and help from this binding, effectively nullifying\n// it. This is a step beyond disabling it, since applications can enable\n// or disable key bindings based on application state.\nfunc (b *Binding) Unbind() {\n\tb.keys = nil\n\tb.help = Help{}\n}\n\n// Help is help information for a given keybinding.\ntype Help struct {\n\tKey  string\n\tDesc string\n}\n\n// Matches checks if the given key matches the given bindings.\nfunc Matches[Key fmt.Stringer](k Key, b ...Binding) bool {\n\tkeys := k.String()\n\tfor _, binding := range b {\n\t\tfor _, v := range binding.keys {\n\t\t\tif keys == v && binding.Enabled() {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "key/key_test.go",
    "content": "package key\n\nimport (\n\t\"testing\"\n)\n\nfunc TestBinding_Enabled(t *testing.T) {\n\tbinding := NewBinding(\n\t\tWithKeys(\"k\", \"up\"),\n\t\tWithHelp(\"↑/k\", \"move up\"),\n\t)\n\tif !binding.Enabled() {\n\t\tt.Errorf(\"expected key to be Enabled\")\n\t}\n\n\tbinding.SetEnabled(false)\n\tif binding.Enabled() {\n\t\tt.Errorf(\"expected key not to be Enabled\")\n\t}\n\n\tbinding.SetEnabled(true)\n\tbinding.Unbind()\n\tif binding.Enabled() {\n\t\tt.Errorf(\"expected key not to be Enabled\")\n\t}\n}\n"
  },
  {
    "path": "list/README.md",
    "content": "# Frequently Asked Questions\n\nThese are some of the most commonly asked questions regarding the `list` bubble.\n\n## Adding Custom Items\n\nThere are a few things you need to do to create custom items. First off, they\nneed to implement the `list.Item` and `list.DefaultItem` interfaces.\n\n```go\n// Item is an item that appears in the list.\ntype Item interface {\n\t// FilterValue is the value we use when filtering against this item when\n\t// we're filtering the list.\n\tFilterValue() string\n}\n```\n\n```go\n// DefaultItem describes an item designed to work with DefaultDelegate.\ntype DefaultItem interface {\n\tItem\n\tTitle() string\n\tDescription() string\n}\n```\n\nYou can see a working example in our [Kancli][kancli] project built\nexplicitly for a tutorial on lists and composite views in Bubble Tea.\n\n[VIDEO](https://youtu.be/ZA93qgdLUzM)\n\n## Customizing Styles\n\nRendering (and behavior) for list items is done via the\n[`ItemDelegate`][itemDelegate]\ninterface. It can be a little confusing at first, but it allows the list to be\nvery flexible and powerful.\n\nIf you just want to alter the default style you could do something like:\n\n```go\nimport \"github.com/charmbracelet/bubbles/v2/list\"\n\n// Create a new default delegate\nd := list.NewDefaultDelegate()\n\n// Change colors\nc := lipgloss.Color(\"#6f03fc\")\nd.Styles.SelectedTitle = d.Styles.SelectedTitle.Foreground(c).BorderLeftForeground(c)\nd.Styles.SelectedDesc = d.Styles.SelectedTitle.Copy() // reuse the title style here\n\n// Initailize the list model with our delegate\nwidth, height := 80, 40\nl := list.New(listItems, d, width, height)\n\n// You can also change the delegate on the fly\nl.SetDelegate(d)\n```\n\nThis code would replace [this line][replacedLine] in the [`list-default`\nexample][listDefault].\n\nFor full control over the way list items are rendered you can also define your\nown `ItemDelegate` too ([example][customDelegate]).\n\n[kancli]: https://github.com/charmbracelet/kancli/blob/main/main.go#L45\n[itemDelegate]: https://pkg.go.dev/github.com/charmbracelet/bubbles/list#ItemDelegate\n[replacedLine]: https://github.com/charmbracelet/bubbletea/blob/main/examples/list-default/main.go#L77\n[listDefault]: https://github.com/charmbracelet/bubbletea/tree/main/examples/list-default\n[customDelegate]: https://github.com/charmbracelet/bubbletea/blob/main/examples/list-simple/main.go#L29-L50\n"
  },
  {
    "path": "list/defaultitem.go",
    "content": "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/lipgloss/v2\"\n\t\"github.com/charmbracelet/x/ansi\"\n)\n\n// DefaultItemStyles defines styling for a default list item.\n// See DefaultItemView for when these come into play.\ntype DefaultItemStyles struct {\n\t// The Normal state.\n\tNormalTitle lipgloss.Style\n\tNormalDesc  lipgloss.Style\n\n\t// The selected item state.\n\tSelectedTitle lipgloss.Style\n\tSelectedDesc  lipgloss.Style\n\n\t// The dimmed state, for when the filter input is initially activated.\n\tDimmedTitle lipgloss.Style\n\tDimmedDesc  lipgloss.Style\n\n\t// Characters matching the current filter, if any.\n\tFilterMatch lipgloss.Style\n}\n\n// NewDefaultItemStyles returns style definitions for a default item. See\n// DefaultItemView for when these come into play.\nfunc NewDefaultItemStyles(isDark bool) (s DefaultItemStyles) {\n\tlightDark := lipgloss.LightDark(isDark)\n\n\ts.NormalTitle = lipgloss.NewStyle().\n\t\tForeground(lightDark(lipgloss.Color(\"#1a1a1a\"), lipgloss.Color(\"#dddddd\"))).\n\t\tPadding(0, 0, 0, 2) //nolint:mnd\n\n\ts.NormalDesc = s.NormalTitle.\n\t\tForeground(lightDark(lipgloss.Color(\"#A49FA5\"), lipgloss.Color(\"#777777\")))\n\n\ts.SelectedTitle = lipgloss.NewStyle().\n\t\tBorder(lipgloss.NormalBorder(), false, false, false, true).\n\t\tBorderForeground(lightDark(lipgloss.Color(\"#F793FF\"), lipgloss.Color(\"#AD58B4\"))).\n\t\tForeground(lightDark(lipgloss.Color(\"#EE6FF8\"), lipgloss.Color(\"#EE6FF8\"))).\n\t\tPadding(0, 0, 0, 1)\n\n\ts.SelectedDesc = s.SelectedTitle.\n\t\tForeground(lightDark(lipgloss.Color(\"#F793FF\"), lipgloss.Color(\"#AD58B4\")))\n\n\ts.DimmedTitle = lipgloss.NewStyle().\n\t\tForeground(lightDark(lipgloss.Color(\"#A49FA5\"), lipgloss.Color(\"#777777\"))).\n\t\tPadding(0, 0, 0, 2) //nolint:mnd\n\n\ts.DimmedDesc = s.DimmedTitle.\n\t\tForeground(lightDark(lipgloss.Color(\"#C2B8C2\"), lipgloss.Color(\"#4D4D4D\")))\n\n\ts.FilterMatch = lipgloss.NewStyle().Underline(true)\n\n\treturn s\n}\n\n// DefaultItem describes an item designed to work with DefaultDelegate.\ntype DefaultItem interface {\n\tItem\n\tTitle() string\n\tDescription() string\n}\n\n// DefaultDelegate is a standard delegate designed to work in lists. It's\n// styled by DefaultItemStyles, which can be customized as you like.\n//\n// The description line can be hidden by setting Description to false, which\n// renders the list as single-line-items. The spacing between items can be set\n// with the SetSpacing method.\n//\n// Setting UpdateFunc is optional. If it's set it will be called when the\n// ItemDelegate called, which is called when the list's Update function is\n// invoked.\n//\n// Settings ShortHelpFunc and FullHelpFunc is optional. They can be set to\n// include items in the list's default short and full help menus.\ntype DefaultDelegate struct {\n\tShowDescription bool\n\tStyles          DefaultItemStyles\n\tUpdateFunc      func(tea.Msg, *Model) tea.Cmd\n\tShortHelpFunc   func() []key.Binding\n\tFullHelpFunc    func() [][]key.Binding\n\theight          int\n\tspacing         int\n}\n\n// NewDefaultDelegate creates a new delegate with default styles.\nfunc NewDefaultDelegate() DefaultDelegate {\n\tconst defaultHeight = 2\n\tconst defaultSpacing = 1\n\treturn DefaultDelegate{\n\t\tShowDescription: true,\n\t\t// XXX: Let the user choose between light and dark colors. We've\n\t\t// temporarily hardcoded the dark colors here.\n\t\tStyles:  NewDefaultItemStyles(true),\n\t\theight:  defaultHeight,\n\t\tspacing: defaultSpacing,\n\t}\n}\n\n// SetHeight sets delegate's preferred height.\nfunc (d *DefaultDelegate) SetHeight(i int) {\n\td.height = i\n}\n\n// Height returns the delegate's preferred height.\n// This has effect only if ShowDescription is true,\n// otherwise height is always 1.\nfunc (d DefaultDelegate) Height() int {\n\tif d.ShowDescription {\n\t\treturn d.height\n\t}\n\treturn 1\n}\n\n// SetSpacing sets the delegate's spacing.\nfunc (d *DefaultDelegate) SetSpacing(i int) {\n\td.spacing = i\n}\n\n// Spacing returns the delegate's spacing.\nfunc (d DefaultDelegate) Spacing() int {\n\treturn d.spacing\n}\n\n// Update checks whether the delegate's UpdateFunc is set and calls it.\nfunc (d DefaultDelegate) Update(msg tea.Msg, m *Model) tea.Cmd {\n\tif d.UpdateFunc == nil {\n\t\treturn nil\n\t}\n\treturn d.UpdateFunc(msg, m)\n}\n\n// Render prints an item.\nfunc (d DefaultDelegate) Render(w io.Writer, m Model, index int, item Item) {\n\tvar (\n\t\ttitle, desc  string\n\t\tmatchedRunes []int\n\t\ts            = &d.Styles\n\t)\n\n\tif i, ok := item.(DefaultItem); ok {\n\t\ttitle = i.Title()\n\t\tdesc = i.Description()\n\t} else {\n\t\treturn\n\t}\n\n\tif m.width <= 0 {\n\t\t// short-circuit\n\t\treturn\n\t}\n\n\t// Prevent text from exceeding list width\n\ttextwidth := m.width - s.NormalTitle.GetPaddingLeft() - s.NormalTitle.GetPaddingRight()\n\ttitle = ansi.Truncate(title, textwidth, ellipsis)\n\tif d.ShowDescription {\n\t\tvar lines []string\n\t\tfor i, line := range strings.Split(desc, \"\\n\") {\n\t\t\tif i >= d.height-1 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tlines = append(lines, ansi.Truncate(line, textwidth, ellipsis))\n\t\t}\n\t\tdesc = strings.Join(lines, \"\\n\")\n\t}\n\n\t// Conditions\n\tvar (\n\t\tisSelected  = index == m.Index()\n\t\temptyFilter = m.FilterState() == Filtering && m.FilterValue() == \"\"\n\t\tisFiltered  = m.FilterState() == Filtering || m.FilterState() == FilterApplied\n\t)\n\n\tif isFiltered && index < len(m.filteredItems) {\n\t\t// Get indices of matched characters\n\t\tmatchedRunes = m.MatchesForItem(index)\n\t}\n\n\tif emptyFilter {\n\t\ttitle = s.DimmedTitle.Render(title)\n\t\tdesc = s.DimmedDesc.Render(desc)\n\t} else if isSelected && m.FilterState() != Filtering {\n\t\tif isFiltered {\n\t\t\t// Highlight matches\n\t\t\tunmatched := s.SelectedTitle.Inline(true)\n\t\t\tmatched := unmatched.Inherit(s.FilterMatch)\n\t\t\ttitle = lipgloss.StyleRunes(title, matchedRunes, matched, unmatched)\n\t\t}\n\t\ttitle = s.SelectedTitle.Render(title)\n\t\tdesc = s.SelectedDesc.Render(desc)\n\t} else {\n\t\tif isFiltered {\n\t\t\t// Highlight matches\n\t\t\tunmatched := s.NormalTitle.Inline(true)\n\t\t\tmatched := unmatched.Inherit(s.FilterMatch)\n\t\t\ttitle = lipgloss.StyleRunes(title, matchedRunes, matched, unmatched)\n\t\t}\n\t\ttitle = s.NormalTitle.Render(title)\n\t\tdesc = s.NormalDesc.Render(desc)\n\t}\n\n\tif d.ShowDescription {\n\t\tfmt.Fprintf(w, \"%s\\n%s\", title, desc) //nolint: errcheck\n\t\treturn\n\t}\n\tfmt.Fprintf(w, \"%s\", title) //nolint: errcheck\n}\n\n// ShortHelp returns the delegate's short help.\nfunc (d DefaultDelegate) ShortHelp() []key.Binding {\n\tif d.ShortHelpFunc != nil {\n\t\treturn d.ShortHelpFunc()\n\t}\n\treturn nil\n}\n\n// FullHelp returns the delegate's full help.\nfunc (d DefaultDelegate) FullHelp() [][]key.Binding {\n\tif d.FullHelpFunc != nil {\n\t\treturn d.FullHelpFunc()\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "list/keys.go",
    "content": "package list\n\nimport \"charm.land/bubbles/v2/key\"\n\n// KeyMap defines keybindings. It satisfies to the help.KeyMap interface, which\n// is used to render the menu.\ntype KeyMap struct {\n\t// Keybindings used when browsing the list.\n\tCursorUp    key.Binding\n\tCursorDown  key.Binding\n\tNextPage    key.Binding\n\tPrevPage    key.Binding\n\tGoToStart   key.Binding\n\tGoToEnd     key.Binding\n\tFilter      key.Binding\n\tClearFilter key.Binding\n\n\t// Keybindings used when setting a filter.\n\tCancelWhileFiltering key.Binding\n\tAcceptWhileFiltering key.Binding\n\n\t// Help toggle keybindings.\n\tShowFullHelp  key.Binding\n\tCloseFullHelp key.Binding\n\n\t// The quit keybinding. This won't be caught when filtering.\n\tQuit key.Binding\n\n\t// The quit-no-matter-what keybinding. This will be caught when filtering.\n\tForceQuit key.Binding\n}\n\n// DefaultKeyMap returns a default set of keybindings.\nfunc DefaultKeyMap() KeyMap {\n\treturn KeyMap{\n\t\t// Browsing.\n\t\tCursorUp: key.NewBinding(\n\t\t\tkey.WithKeys(\"up\", \"k\"),\n\t\t\tkey.WithHelp(\"↑/k\", \"up\"),\n\t\t),\n\t\tCursorDown: key.NewBinding(\n\t\t\tkey.WithKeys(\"down\", \"j\"),\n\t\t\tkey.WithHelp(\"↓/j\", \"down\"),\n\t\t),\n\t\tPrevPage: key.NewBinding(\n\t\t\tkey.WithKeys(\"left\", \"h\", \"pgup\", \"b\", \"u\"),\n\t\t\tkey.WithHelp(\"←/h/pgup\", \"prev page\"),\n\t\t),\n\t\tNextPage: key.NewBinding(\n\t\t\tkey.WithKeys(\"right\", \"l\", \"pgdown\", \"f\", \"d\"),\n\t\t\tkey.WithHelp(\"→/l/pgdn\", \"next page\"),\n\t\t),\n\t\tGoToStart: key.NewBinding(\n\t\t\tkey.WithKeys(\"home\", \"g\"),\n\t\t\tkey.WithHelp(\"g/home\", \"go to start\"),\n\t\t),\n\t\tGoToEnd: key.NewBinding(\n\t\t\tkey.WithKeys(\"end\", \"G\"),\n\t\t\tkey.WithHelp(\"G/end\", \"go to end\"),\n\t\t),\n\t\tFilter: key.NewBinding(\n\t\t\tkey.WithKeys(\"/\"),\n\t\t\tkey.WithHelp(\"/\", \"filter\"),\n\t\t),\n\t\tClearFilter: key.NewBinding(\n\t\t\tkey.WithKeys(\"esc\"),\n\t\t\tkey.WithHelp(\"esc\", \"clear filter\"),\n\t\t),\n\n\t\t// Filtering.\n\t\tCancelWhileFiltering: key.NewBinding(\n\t\t\tkey.WithKeys(\"esc\"),\n\t\t\tkey.WithHelp(\"esc\", \"cancel\"),\n\t\t),\n\t\tAcceptWhileFiltering: key.NewBinding(\n\t\t\tkey.WithKeys(\"enter\", \"tab\", \"shift+tab\", \"ctrl+k\", \"up\", \"ctrl+j\", \"down\"),\n\t\t\tkey.WithHelp(\"enter\", \"apply filter\"),\n\t\t),\n\n\t\t// Toggle help.\n\t\tShowFullHelp: key.NewBinding(\n\t\t\tkey.WithKeys(\"?\"),\n\t\t\tkey.WithHelp(\"?\", \"more\"),\n\t\t),\n\t\tCloseFullHelp: key.NewBinding(\n\t\t\tkey.WithKeys(\"?\"),\n\t\t\tkey.WithHelp(\"?\", \"close help\"),\n\t\t),\n\n\t\t// Quitting.\n\t\tQuit: key.NewBinding(\n\t\t\tkey.WithKeys(\"q\", \"esc\"),\n\t\t\tkey.WithHelp(\"q\", \"quit\"),\n\t\t),\n\t\tForceQuit: key.NewBinding(key.WithKeys(\"ctrl+c\")),\n\t}\n}\n"
  },
  {
    "path": "list/list.go",
    "content": "// Package list provides a feature-rich Bubble Tea component for browsing\n// a general purpose list of items. It features optional filtering, pagination,\n// help, status messages, and a spinner to indicate activity.\npackage list\n\nimport (\n\t\"cmp\"\n\t\"fmt\"\n\t\"io\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\ttea \"charm.land/bubbletea/v2\"\n\t\"charm.land/lipgloss/v2\"\n\t\"github.com/charmbracelet/x/ansi\"\n\t\"github.com/sahilm/fuzzy\"\n\n\t\"charm.land/bubbles/v2/help\"\n\t\"charm.land/bubbles/v2/key\"\n\t\"charm.land/bubbles/v2/paginator\"\n\t\"charm.land/bubbles/v2/spinner\"\n\t\"charm.land/bubbles/v2/textinput\"\n)\n\nfunc clamp[T cmp.Ordered](v, low, high T) T {\n\tif low > high {\n\t\tlow, high = high, low\n\t}\n\treturn min(high, max(low, v))\n}\n\n// Item is an item that appears in the list.\ntype Item interface {\n\t// FilterValue is the value we use when filtering against this item when\n\t// we're filtering the list.\n\tFilterValue() string\n}\n\n// ItemDelegate encapsulates the general functionality for all list items. The\n// benefit to separating this logic from the item itself is that you can change\n// the functionality of items without changing the actual items themselves.\n//\n// Note that if the delegate also implements help.KeyMap delegate-related\n// help items will be added to the help view.\ntype ItemDelegate interface {\n\t// Render renders the item's view.\n\tRender(w io.Writer, m Model, index int, item Item)\n\n\t// Height is the height of the list item.\n\tHeight() int\n\n\t// Spacing is the size of the horizontal gap between list items in cells.\n\tSpacing() int\n\n\t// Update is the update loop for items. All messages in the list's update\n\t// loop will pass through here except when the user is setting a filter.\n\t// Use this method to perform item-level updates appropriate to this\n\t// delegate.\n\tUpdate(msg tea.Msg, m *Model) tea.Cmd\n}\n\ntype filteredItem struct {\n\tindex   int   // index in the unfiltered list\n\titem    Item  // item matched\n\tmatches []int // rune indices of matched items\n}\n\ntype filteredItems []filteredItem\n\nfunc (f filteredItems) items() []Item {\n\tagg := make([]Item, len(f))\n\tfor i, v := range f {\n\t\tagg[i] = v.item\n\t}\n\treturn agg\n}\n\n// FilterMatchesMsg contains data about items matched during filtering. The\n// message should be routed to Update for processing.\ntype FilterMatchesMsg []filteredItem\n\n// FilterFunc takes a term and a list of strings to search through\n// (defined by Item#FilterValue).\n// It should return a sorted list of ranks.\ntype FilterFunc func(string, []string) []Rank\n\n// Rank defines a rank for a given item.\ntype Rank struct {\n\t// The index of the item in the original input.\n\tIndex int\n\t// Indices of the actual word that were matched against the filter term.\n\tMatchedIndexes []int\n}\n\n// DefaultFilter uses the sahilm/fuzzy to filter through the list.\n// This is set by default.\nfunc DefaultFilter(term string, targets []string) []Rank {\n\tranks := fuzzy.Find(term, targets)\n\tsort.Stable(ranks)\n\tresult := make([]Rank, len(ranks))\n\tfor i, r := range ranks {\n\t\tresult[i] = Rank{\n\t\t\tIndex:          r.Index,\n\t\t\tMatchedIndexes: r.MatchedIndexes,\n\t\t}\n\t}\n\treturn result\n}\n\n// UnsortedFilter uses the sahilm/fuzzy to filter through the list. It does not\n// sort the results.\nfunc UnsortedFilter(term string, targets []string) []Rank {\n\tranks := fuzzy.FindNoSort(term, targets)\n\tresult := make([]Rank, len(ranks))\n\tfor i, r := range ranks {\n\t\tresult[i] = Rank{\n\t\t\tIndex:          r.Index,\n\t\t\tMatchedIndexes: r.MatchedIndexes,\n\t\t}\n\t}\n\treturn result\n}\n\ntype statusMessageTimeoutMsg struct{}\n\n// FilterState describes the current filtering state on the model.\ntype FilterState int\n\n// Possible filter states.\nconst (\n\tUnfiltered    FilterState = iota // no filter set\n\tFiltering                        // user is actively setting a filter\n\tFilterApplied                    // a filter is applied and user is not editing filter\n)\n\n// String returns a human-readable string of the current filter state.\nfunc (f FilterState) String() string {\n\treturn [...]string{\n\t\t\"unfiltered\",\n\t\t\"filtering\",\n\t\t\"filter applied\",\n\t}[f]\n}\n\n// Model contains the state of this component.\ntype Model struct {\n\tshowTitle        bool\n\tshowFilter       bool\n\tshowStatusBar    bool\n\tshowPagination   bool\n\tshowHelp         bool\n\tfilteringEnabled bool\n\n\titemNameSingular string\n\titemNamePlural   string\n\n\tTitle             string\n\tStyles            Styles\n\tInfiniteScrolling bool\n\n\t// Key mappings for navigating the list.\n\tKeyMap KeyMap\n\n\t// Filter is used to filter the list.\n\tFilter FilterFunc\n\n\tdisableQuitKeybindings bool\n\n\t// Additional key mappings for the short and full help views. This allows\n\t// you to add additional key mappings to the help menu without\n\t// re-implementing the help component. Of course, you can also disable the\n\t// list's help component and implement a new one if you need more\n\t// flexibility.\n\tAdditionalShortHelpKeys func() []key.Binding\n\tAdditionalFullHelpKeys  func() []key.Binding\n\n\tspinner     spinner.Model\n\tshowSpinner bool\n\twidth       int\n\theight      int\n\tPaginator   paginator.Model\n\tcursor      int\n\tHelp        help.Model\n\tFilterInput textinput.Model\n\tfilterState FilterState\n\n\t// How long status messages should stay visible. By default this is\n\t// 1 second.\n\tStatusMessageLifetime time.Duration\n\n\tstatusMessage      string\n\tstatusMessageTimer *time.Timer\n\n\t// The master set of items we're working with.\n\titems []Item\n\n\t// Filtered items we're currently displaying. Filtering, toggles and so on\n\t// will alter this slice so we can show what is relevant. For that reason,\n\t// this field should be considered ephemeral.\n\tfilteredItems filteredItems\n\n\tdelegate ItemDelegate\n}\n\n// New returns a new model with sensible defaults.\nfunc New(items []Item, delegate ItemDelegate, width, height int) Model {\n\t// XXX: Let the user choose between light and dark colors. We've\n\t// temporarily hardcoded the dark colors here.\n\tstyles := DefaultStyles(true)\n\n\tsp := spinner.New()\n\tsp.Spinner = spinner.Line\n\tsp.Style = styles.Spinner\n\n\tfilterInput := textinput.New()\n\tfilterInput.Prompt = \"Filter: \"\n\tfilterInput.CharLimit = 64\n\tfilterInput.Focus()\n\n\tp := paginator.New()\n\tp.Type = paginator.Dots\n\tp.ActiveDot = styles.ActivePaginationDot.String()\n\tp.InactiveDot = styles.InactivePaginationDot.String()\n\n\tm := Model{\n\t\tshowTitle:             true,\n\t\tshowFilter:            true,\n\t\tshowStatusBar:         true,\n\t\tshowPagination:        true,\n\t\tshowHelp:              true,\n\t\titemNameSingular:      \"item\",\n\t\titemNamePlural:        \"items\",\n\t\tfilteringEnabled:      true,\n\t\tKeyMap:                DefaultKeyMap(),\n\t\tFilter:                DefaultFilter,\n\t\tStyles:                styles,\n\t\tTitle:                 \"List\",\n\t\tFilterInput:           filterInput,\n\t\tStatusMessageLifetime: time.Second,\n\n\t\twidth:     width,\n\t\theight:    height,\n\t\tdelegate:  delegate,\n\t\titems:     items,\n\t\tPaginator: p,\n\t\tspinner:   sp,\n\t\tHelp:      help.New(),\n\t}\n\n\tm.updatePagination()\n\tm.updateKeybindings()\n\treturn m\n}\n\n// SetFilteringEnabled enables or disables filtering. Note that this is different\n// from ShowFilter, which merely hides or shows the input view.\nfunc (m *Model) SetFilteringEnabled(v bool) {\n\tm.filteringEnabled = v\n\tif !v {\n\t\tm.resetFiltering()\n\t}\n\tm.updateKeybindings()\n}\n\n// FilteringEnabled returns whether or not filtering is enabled.\nfunc (m Model) FilteringEnabled() bool {\n\treturn m.filteringEnabled\n}\n\n// SetShowTitle shows or hides the title bar.\nfunc (m *Model) SetShowTitle(v bool) {\n\tm.showTitle = v\n\tm.updatePagination()\n}\n\n// SetFilterText explicitly sets the filter text without relying on user input.\n// It also sets the filterState to a sane default of FilterApplied, but this\n// can be changed with SetFilterState.\nfunc (m *Model) SetFilterText(filter string) {\n\tm.filterState = Filtering\n\tm.FilterInput.SetValue(filter)\n\tcmd := filterItems(*m)\n\tmsg := cmd()\n\tfmm, _ := msg.(FilterMatchesMsg)\n\tm.filteredItems = filteredItems(fmm)\n\tm.filterState = FilterApplied\n\tm.GoToStart()\n\tm.FilterInput.CursorEnd()\n\tm.updatePagination()\n\tm.updateKeybindings()\n}\n\n// SetFilterState allows setting the filtering state manually.\nfunc (m *Model) SetFilterState(state FilterState) {\n\tm.GoToStart()\n\tm.filterState = state\n\tm.FilterInput.CursorEnd()\n\tm.FilterInput.Focus()\n\tm.updateKeybindings()\n}\n\n// ShowTitle returns whether or not the title bar is set to be rendered.\nfunc (m Model) ShowTitle() bool {\n\treturn m.showTitle\n}\n\n// SetShowFilter shows or hides the filter bar. Note that this does not disable\n// filtering, it simply hides the built-in filter view. This allows you to\n// use the FilterInput to render the filtering UI differently without having to\n// re-implement filtering from scratch.\n//\n// To disable filtering entirely use EnableFiltering.\nfunc (m *Model) SetShowFilter(v bool) {\n\tm.showFilter = v\n\tm.updatePagination()\n}\n\n// ShowFilter returns whether or not the filter is set to be rendered. Note\n// that this is separate from FilteringEnabled, so filtering can be hidden yet\n// still invoked. This allows you to render filtering differently without\n// having to re-implement it from scratch.\nfunc (m Model) ShowFilter() bool {\n\treturn m.showFilter\n}\n\n// SetShowStatusBar shows or hides the view that displays metadata about the\n// list, such as item counts.\nfunc (m *Model) SetShowStatusBar(v bool) {\n\tm.showStatusBar = v\n\tm.updatePagination()\n}\n\n// ShowStatusBar returns whether or not the status bar is set to be rendered.\nfunc (m Model) ShowStatusBar() bool {\n\treturn m.showStatusBar\n}\n\n// SetStatusBarItemName defines a replacement for the item's identifier.\n// Defaults to item/items.\nfunc (m *Model) SetStatusBarItemName(singular, plural string) {\n\tm.itemNameSingular = singular\n\tm.itemNamePlural = plural\n}\n\n// StatusBarItemName returns singular and plural status bar item names.\nfunc (m Model) StatusBarItemName() (string, string) {\n\treturn m.itemNameSingular, m.itemNamePlural\n}\n\n// SetShowPagination hides or shows the paginator. Note that pagination will\n// still be active, it simply won't be displayed.\nfunc (m *Model) SetShowPagination(v bool) {\n\tm.showPagination = v\n\tm.updatePagination()\n}\n\n// ShowPagination returns whether the pagination is visible.\nfunc (m *Model) ShowPagination() bool {\n\treturn m.showPagination\n}\n\n// SetShowHelp shows or hides the help view.\nfunc (m *Model) SetShowHelp(v bool) {\n\tm.showHelp = v\n\tm.updatePagination()\n}\n\n// ShowHelp returns whether or not the help is set to be rendered.\nfunc (m Model) ShowHelp() bool {\n\treturn m.showHelp\n}\n\n// Items returns the items in the list.\nfunc (m Model) Items() []Item {\n\treturn m.items\n}\n\n// SetItems sets the items available in the list. This returns a command.\nfunc (m *Model) SetItems(i []Item) tea.Cmd {\n\tvar cmd tea.Cmd\n\tm.items = i\n\n\tif m.filterState != Unfiltered {\n\t\tm.filteredItems = nil\n\t\tcmd = filterItems(*m)\n\t}\n\n\tm.updatePagination()\n\tm.updateKeybindings()\n\treturn cmd\n}\n\n// Select selects the given index of the list and goes to its respective page.\nfunc (m *Model) Select(index int) {\n\tm.Paginator.Page = index / m.Paginator.PerPage\n\tm.cursor = index % m.Paginator.PerPage\n}\n\n// ResetSelected resets the selected item to the first item in the first page of the list.\nfunc (m *Model) ResetSelected() {\n\tm.Select(0)\n}\n\n// ResetFilter resets the current filtering state.\nfunc (m *Model) ResetFilter() {\n\tm.resetFiltering()\n}\n\n// SetItem replaces an item at the given index. This returns a command.\nfunc (m *Model) SetItem(index int, item Item) tea.Cmd {\n\tvar cmd tea.Cmd\n\tm.items[index] = item\n\n\tif m.filterState != Unfiltered {\n\t\tcmd = filterItems(*m)\n\t}\n\n\tm.updatePagination()\n\treturn cmd\n}\n\n// InsertItem inserts an item at the given index. If the index is out of the upper bound,\n// the item will be appended. This returns a command.\nfunc (m *Model) InsertItem(index int, item Item) tea.Cmd {\n\tvar cmd tea.Cmd\n\tm.items = insertItemIntoSlice(m.items, item, index)\n\n\tif m.filterState != Unfiltered {\n\t\tcmd = filterItems(*m)\n\t}\n\n\tm.updatePagination()\n\tm.updateKeybindings()\n\treturn cmd\n}\n\n// RemoveItem removes an item at the given index. If the index is out of bounds\n// this will be a no-op. O(n) complexity, which probably won't matter in the\n// case of a TUI.\nfunc (m *Model) RemoveItem(index int) {\n\tm.items = removeItemFromSlice(m.items, index)\n\tif m.filterState != Unfiltered {\n\t\tm.filteredItems = removeFilterMatchFromSlice(m.filteredItems, index)\n\t\tif len(m.filteredItems) == 0 {\n\t\t\tm.resetFiltering()\n\t\t}\n\t}\n\tm.updatePagination()\n}\n\n// SetDelegate sets the item delegate.\nfunc (m *Model) SetDelegate(d ItemDelegate) {\n\tm.delegate = d\n\tm.updatePagination()\n}\n\n// VisibleItems returns the total items available to be shown.\nfunc (m Model) VisibleItems() []Item {\n\tif m.filterState != Unfiltered {\n\t\treturn m.filteredItems.items()\n\t}\n\treturn m.items\n}\n\n// SelectedItem returns the current selected item in the list.\nfunc (m Model) SelectedItem() Item {\n\ti := m.Index()\n\n\titems := m.VisibleItems()\n\tif i < 0 || len(items) == 0 || len(items) <= i {\n\t\treturn nil\n\t}\n\n\treturn items[i]\n}\n\n// MatchesForItem returns rune positions matched by the current filter, if any.\n// Use this to style runes matched by the active filter.\n//\n// See DefaultItemView for a usage example.\nfunc (m Model) MatchesForItem(index int) []int {\n\tif m.filteredItems == nil || index >= len(m.filteredItems) {\n\t\treturn nil\n\t}\n\treturn m.filteredItems[index].matches\n}\n\n// Index returns the index of the currently selected item as it is stored in the\n// filtered list of items.\n// Using this value with SetItem() might be incorrect, consider using\n// GlobalIndex() instead.\nfunc (m Model) Index() int {\n\treturn m.Paginator.Page*m.Paginator.PerPage + m.cursor\n}\n\n// GlobalIndex returns the index of the currently selected item as it is stored\n// in the unfiltered list of items. This value can be used with SetItem().\nfunc (m Model) GlobalIndex() int {\n\tindex := m.Index()\n\n\tif m.filteredItems == nil || index >= len(m.filteredItems) {\n\t\treturn index\n\t}\n\n\treturn m.filteredItems[index].index\n}\n\n// Cursor returns the index of the cursor on the current page.\nfunc (m Model) Cursor() int {\n\treturn m.cursor\n}\n\n// CursorUp moves the cursor up. This can also move the state to the previous\n// page.\nfunc (m *Model) CursorUp() {\n\tm.cursor--\n\n\t// If we're at the start, stop\n\tif m.cursor < 0 && m.Paginator.OnFirstPage() {\n\t\t// if infinite scrolling is enabled, go to the last item\n\t\tif m.InfiniteScrolling {\n\t\t\tm.GoToEnd()\n\t\t\treturn\n\t\t}\n\t\tm.cursor = 0\n\t\treturn\n\t}\n\n\t// Move the cursor as normal\n\tif m.cursor >= 0 {\n\t\treturn\n\t}\n\n\t// Go to the previous page\n\tm.Paginator.PrevPage()\n\tm.cursor = m.maxCursorIndex()\n}\n\n// CursorDown moves the cursor down. This can also advance the state to the\n// next page.\nfunc (m *Model) CursorDown() {\n\tmaxCursorIndex := m.maxCursorIndex()\n\n\tm.cursor++\n\n\t// We're still within bounds of the current page, so no need to do anything.\n\tif m.cursor <= maxCursorIndex {\n\t\treturn\n\t}\n\n\t// Go to the next page\n\tif !m.Paginator.OnLastPage() {\n\t\tm.Paginator.NextPage()\n\t\tm.cursor = 0\n\t\treturn\n\t}\n\n\tm.cursor = max(0, maxCursorIndex)\n\n\t// if infinite scrolling is enabled, go to the first item.\n\tif m.InfiniteScrolling {\n\t\tm.GoToStart()\n\t}\n}\n\n// GoToStart moves to the first page, and first item on the first page.\nfunc (m *Model) GoToStart() {\n\tm.Paginator.Page = 0\n\tm.cursor = 0\n}\n\n// GoToEnd moves to the last page, and last item on the last page.\nfunc (m *Model) GoToEnd() {\n\tm.Paginator.Page = max(0, m.Paginator.TotalPages-1)\n\tm.cursor = m.maxCursorIndex()\n}\n\n// PrevPage moves to the previous page, if available.\nfunc (m *Model) PrevPage() {\n\tm.Paginator.PrevPage()\n\tm.cursor = clamp(m.cursor, 0, m.maxCursorIndex())\n}\n\n// NextPage moves to the next page, if available.\nfunc (m *Model) NextPage() {\n\tm.Paginator.NextPage()\n\tm.cursor = clamp(m.cursor, 0, m.maxCursorIndex())\n}\n\nfunc (m *Model) maxCursorIndex() int {\n\treturn max(0, m.Paginator.ItemsOnPage(len(m.VisibleItems()))-1)\n}\n\n// FilterState returns the current filter state.\nfunc (m Model) FilterState() FilterState {\n\treturn m.filterState\n}\n\n// FilterValue returns the current value of the filter.\nfunc (m Model) FilterValue() string {\n\treturn m.FilterInput.Value()\n}\n\n// SettingFilter returns whether or not the user is currently editing the\n// filter value. It's purely a convenience method for the following:\n//\n//\tm.FilterState() == Filtering\n//\n// It's included here because it's a common thing to check for when\n// implementing this component.\nfunc (m Model) SettingFilter() bool {\n\treturn m.filterState == Filtering\n}\n\n// IsFiltered returns whether or not the list is currently filtered.\n// It's purely a convenience method for the following:\n//\n//\tm.FilterState() == FilterApplied\nfunc (m Model) IsFiltered() bool {\n\treturn m.filterState == FilterApplied\n}\n\n// Width returns the current width setting.\nfunc (m Model) Width() int {\n\treturn m.width\n}\n\n// Height returns the current height setting.\nfunc (m Model) Height() int {\n\treturn m.height\n}\n\n// SetSpinner allows to set the spinner style.\nfunc (m *Model) SetSpinner(spinner spinner.Spinner) {\n\tm.spinner.Spinner = spinner\n}\n\n// ToggleSpinner toggles the spinner. Note that this also returns a command.\nfunc (m *Model) ToggleSpinner() tea.Cmd {\n\tif !m.showSpinner {\n\t\treturn m.StartSpinner()\n\t}\n\tm.StopSpinner()\n\treturn nil\n}\n\n// StartSpinner starts the spinner. Note that this returns a command.\nfunc (m *Model) StartSpinner() tea.Cmd {\n\tm.showSpinner = true\n\treturn m.spinner.Tick\n}\n\n// StopSpinner stops the spinner.\nfunc (m *Model) StopSpinner() {\n\tm.showSpinner = false\n}\n\n// DisableQuitKeybindings is a helper for disabling the keybindings used for quitting,\n// in case you want to handle this elsewhere in your application.\nfunc (m *Model) DisableQuitKeybindings() {\n\tm.disableQuitKeybindings = true\n\tm.KeyMap.Quit.SetEnabled(false)\n\tm.KeyMap.ForceQuit.SetEnabled(false)\n}\n\n// NewStatusMessage sets a new status message, which will show for a limited\n// amount of time. Note that this also returns a command.\nfunc (m *Model) NewStatusMessage(s string) tea.Cmd {\n\tm.statusMessage = s\n\tif m.statusMessageTimer != nil {\n\t\tm.statusMessageTimer.Stop()\n\t}\n\n\tm.statusMessageTimer = time.NewTimer(m.StatusMessageLifetime)\n\n\t// Wait for timeout\n\treturn func() tea.Msg {\n\t\t<-m.statusMessageTimer.C\n\t\treturn statusMessageTimeoutMsg{}\n\t}\n}\n\n// SetWidth sets the width of this component.\nfunc (m *Model) SetWidth(v int) {\n\tm.SetSize(v, m.height)\n}\n\n// SetHeight sets the height of this component.\nfunc (m *Model) SetHeight(v int) {\n\tm.SetSize(m.width, v)\n}\n\n// SetSize sets the width and height of this component.\nfunc (m *Model) SetSize(width, height int) {\n\tpromptWidth := lipgloss.Width(m.Styles.Title.Render(m.FilterInput.Prompt))\n\n\tm.width = width\n\tm.height = height\n\tm.Help.SetWidth(width)\n\tm.FilterInput.SetWidth(width - promptWidth - lipgloss.Width(m.spinnerView()))\n\tm.updatePagination()\n\tm.updateKeybindings()\n}\n\nfunc (m *Model) resetFiltering() {\n\tif m.filterState == Unfiltered {\n\t\treturn\n\t}\n\n\tm.filterState = Unfiltered\n\tm.FilterInput.Reset()\n\tm.filteredItems = nil\n\tm.updatePagination()\n\tm.updateKeybindings()\n}\n\nfunc (m Model) itemsAsFilterItems() filteredItems {\n\tfi := make([]filteredItem, len(m.items))\n\tfor i, item := range m.items {\n\t\tfi[i] = filteredItem{\n\t\t\titem: item,\n\t\t}\n\t}\n\treturn fi\n}\n\n// Set keybindings according to the filter state.\nfunc (m *Model) updateKeybindings() {\n\tswitch m.filterState { //nolint:exhaustive\n\tcase Filtering:\n\t\tm.KeyMap.CursorUp.SetEnabled(false)\n\t\tm.KeyMap.CursorDown.SetEnabled(false)\n\t\tm.KeyMap.NextPage.SetEnabled(false)\n\t\tm.KeyMap.PrevPage.SetEnabled(false)\n\t\tm.KeyMap.GoToStart.SetEnabled(false)\n\t\tm.KeyMap.GoToEnd.SetEnabled(false)\n\t\tm.KeyMap.Filter.SetEnabled(false)\n\t\tm.KeyMap.ClearFilter.SetEnabled(false)\n\t\tm.KeyMap.CancelWhileFiltering.SetEnabled(true)\n\t\tm.KeyMap.AcceptWhileFiltering.SetEnabled(m.FilterInput.Value() != \"\")\n\t\tm.KeyMap.Quit.SetEnabled(false)\n\t\tm.KeyMap.ShowFullHelp.SetEnabled(false)\n\t\tm.KeyMap.CloseFullHelp.SetEnabled(false)\n\n\tdefault:\n\t\thasItems := len(m.items) != 0\n\t\tm.KeyMap.CursorUp.SetEnabled(hasItems)\n\t\tm.KeyMap.CursorDown.SetEnabled(hasItems)\n\n\t\thasPages := m.Paginator.TotalPages > 1\n\t\tm.KeyMap.NextPage.SetEnabled(hasPages)\n\t\tm.KeyMap.PrevPage.SetEnabled(hasPages)\n\n\t\tm.KeyMap.GoToStart.SetEnabled(hasItems)\n\t\tm.KeyMap.GoToEnd.SetEnabled(hasItems)\n\n\t\tm.KeyMap.Filter.SetEnabled(m.filteringEnabled && hasItems)\n\t\tm.KeyMap.ClearFilter.SetEnabled(m.filterState == FilterApplied)\n\t\tm.KeyMap.CancelWhileFiltering.SetEnabled(false)\n\t\tm.KeyMap.AcceptWhileFiltering.SetEnabled(false)\n\t\tm.KeyMap.Quit.SetEnabled(!m.disableQuitKeybindings)\n\n\t\tif m.Help.ShowAll {\n\t\t\tm.KeyMap.ShowFullHelp.SetEnabled(true)\n\t\t\tm.KeyMap.CloseFullHelp.SetEnabled(true)\n\t\t} else {\n\t\t\tminHelp := countEnabledBindings(m.FullHelp()) > 1\n\t\t\tm.KeyMap.ShowFullHelp.SetEnabled(minHelp)\n\t\t\tm.KeyMap.CloseFullHelp.SetEnabled(minHelp)\n\t\t}\n\t}\n}\n\n// Update pagination according to the amount of items for the current state.\nfunc (m *Model) updatePagination() {\n\tindex := m.Index()\n\tavailHeight := m.height\n\n\tif m.showTitle || (m.showFilter && m.filteringEnabled) {\n\t\tavailHeight -= lipgloss.Height(m.titleView())\n\t}\n\tif m.showStatusBar {\n\t\tavailHeight -= lipgloss.Height(m.statusView())\n\t}\n\tif m.showPagination {\n\t\tavailHeight -= lipgloss.Height(m.paginationView())\n\t}\n\tif m.showHelp {\n\t\tavailHeight -= lipgloss.Height(m.helpView())\n\t}\n\n\tm.Paginator.PerPage = max(1, availHeight/(m.delegate.Height()+m.delegate.Spacing()))\n\n\tif pages := len(m.VisibleItems()); pages < 1 {\n\t\tm.Paginator.SetTotalPages(1)\n\t} else {\n\t\tm.Paginator.SetTotalPages(pages)\n\t}\n\n\t// Restore index\n\tm.Paginator.Page = index / m.Paginator.PerPage\n\tm.cursor = index % m.Paginator.PerPage\n\n\t// Make sure the page stays in bounds\n\tif m.Paginator.Page >= m.Paginator.TotalPages-1 {\n\t\tm.Paginator.Page = max(0, m.Paginator.TotalPages-1)\n\t}\n}\n\nfunc (m *Model) hideStatusMessage() {\n\tm.statusMessage = \"\"\n\tif m.statusMessageTimer != nil {\n\t\tm.statusMessageTimer.Stop()\n\t}\n}\n\n// Update is the Bubble Tea update loop.\nfunc (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {\n\tvar cmds []tea.Cmd\n\n\tswitch msg := msg.(type) {\n\tcase tea.KeyPressMsg:\n\t\tif key.Matches(msg, m.KeyMap.ForceQuit) {\n\t\t\treturn m, tea.Quit\n\t\t}\n\n\tcase FilterMatchesMsg:\n\t\tm.filteredItems = filteredItems(msg)\n\t\treturn m, nil\n\n\tcase spinner.TickMsg:\n\t\tnewSpinnerModel, cmd := m.spinner.Update(msg)\n\t\tm.spinner = newSpinnerModel\n\t\tif m.showSpinner {\n\t\t\tcmds = append(cmds, cmd)\n\t\t}\n\n\tcase statusMessageTimeoutMsg:\n\t\tm.hideStatusMessage()\n\t}\n\n\tif m.filterState == Filtering {\n\t\tcmds = append(cmds, m.handleFiltering(msg))\n\t} else {\n\t\tcmds = append(cmds, m.handleBrowsing(msg))\n\t}\n\n\treturn m, tea.Batch(cmds...)\n}\n\n// Updates for when a user is browsing the list.\nfunc (m *Model) handleBrowsing(msg tea.Msg) tea.Cmd {\n\tswitch msg := msg.(type) {\n\tcase tea.KeyPressMsg:\n\t\tswitch {\n\t\t// Note: we match clear filter before quit because, by default, they're\n\t\t// both mapped to escape.\n\t\tcase key.Matches(msg, m.KeyMap.ClearFilter):\n\t\t\tm.resetFiltering()\n\n\t\tcase key.Matches(msg, m.KeyMap.Quit):\n\t\t\treturn tea.Quit\n\n\t\tcase key.Matches(msg, m.KeyMap.CursorUp):\n\t\t\tm.CursorUp()\n\n\t\tcase key.Matches(msg, m.KeyMap.CursorDown):\n\t\t\tm.CursorDown()\n\n\t\tcase key.Matches(msg, m.KeyMap.PrevPage):\n\t\t\tm.Paginator.PrevPage()\n\n\t\tcase key.Matches(msg, m.KeyMap.NextPage):\n\t\t\tm.Paginator.NextPage()\n\n\t\tcase key.Matches(msg, m.KeyMap.GoToStart):\n\t\t\tm.GoToStart()\n\n\t\tcase key.Matches(msg, m.KeyMap.GoToEnd):\n\t\t\tm.GoToEnd()\n\n\t\tcase key.Matches(msg, m.KeyMap.Filter):\n\t\t\tm.hideStatusMessage()\n\t\t\tif m.FilterInput.Value() == \"\" {\n\t\t\t\t// Populate filter with all items only if the filter is empty.\n\t\t\t\tm.filteredItems = m.itemsAsFilterItems()\n\t\t\t}\n\t\t\tm.GoToStart()\n\t\t\tm.filterState = Filtering\n\t\t\tm.FilterInput.CursorEnd()\n\t\t\tm.FilterInput.Focus()\n\t\t\tm.updateKeybindings()\n\t\t\treturn textinput.Blink\n\n\t\tcase key.Matches(msg, m.KeyMap.ShowFullHelp):\n\t\t\tfallthrough\n\t\tcase key.Matches(msg, m.KeyMap.CloseFullHelp):\n\t\t\tm.Help.ShowAll = !m.Help.ShowAll\n\t\t\tm.updatePagination()\n\t\t}\n\t}\n\n\tcmd := m.delegate.Update(msg, m)\n\tm.cursor = clamp(m.cursor, 0, m.maxCursorIndex())\n\n\treturn cmd\n}\n\n// Updates for when a user is in the filter editing interface.\nfunc (m *Model) handleFiltering(msg tea.Msg) tea.Cmd {\n\tvar cmds []tea.Cmd\n\n\t// Handle keys\n\tif msg, ok := msg.(tea.KeyPressMsg); ok {\n\t\tswitch {\n\t\tcase key.Matches(msg, m.KeyMap.CancelWhileFiltering):\n\t\t\tm.resetFiltering()\n\t\t\tm.KeyMap.Filter.SetEnabled(true)\n\t\t\tm.KeyMap.ClearFilter.SetEnabled(false)\n\n\t\tcase key.Matches(msg, m.KeyMap.AcceptWhileFiltering):\n\t\t\tm.hideStatusMessage()\n\n\t\t\tif len(m.items) == 0 {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\th := m.VisibleItems()\n\n\t\t\t// If we've filtered down to nothing, clear the filter\n\t\t\tif len(h) == 0 {\n\t\t\t\tm.resetFiltering()\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tm.FilterInput.Blur()\n\t\t\tm.filterState = FilterApplied\n\t\t\tm.updateKeybindings()\n\n\t\t\tif m.FilterInput.Value() == \"\" {\n\t\t\t\tm.resetFiltering()\n\t\t\t}\n\t\t}\n\t}\n\n\t// Update the filter text input component\n\tnewFilterInputModel, inputCmd := m.FilterInput.Update(msg)\n\tfilterChanged := m.FilterInput.Value() != newFilterInputModel.Value()\n\tm.FilterInput = newFilterInputModel\n\tcmds = append(cmds, inputCmd)\n\n\t// If the filtering input has changed, request updated filtering\n\tif filterChanged {\n\t\tcmds = append(cmds, filterItems(*m))\n\t\tm.KeyMap.AcceptWhileFiltering.SetEnabled(m.FilterInput.Value() != \"\")\n\t}\n\n\t// Update pagination\n\tm.updatePagination()\n\n\treturn tea.Batch(cmds...)\n}\n\n// ShortHelp returns bindings to show in the abbreviated help view. It's part\n// of the help.KeyMap interface.\nfunc (m Model) ShortHelp() []key.Binding {\n\tkb := []key.Binding{\n\t\tm.KeyMap.CursorUp,\n\t\tm.KeyMap.CursorDown,\n\t}\n\n\tfiltering := m.filterState == Filtering\n\n\t// If the delegate implements the help.KeyMap interface add the short help\n\t// items to the short help after the cursor movement keys.\n\tif !filtering {\n\t\tif b, ok := m.delegate.(help.KeyMap); ok {\n\t\t\tkb = append(kb, b.ShortHelp()...)\n\t\t}\n\t}\n\n\tkb = append(kb,\n\t\tm.KeyMap.Filter,\n\t\tm.KeyMap.ClearFilter,\n\t\tm.KeyMap.AcceptWhileFiltering,\n\t\tm.KeyMap.CancelWhileFiltering,\n\t)\n\n\tif !filtering && m.AdditionalShortHelpKeys != nil {\n\t\tkb = append(kb, m.AdditionalShortHelpKeys()...)\n\t}\n\n\treturn append(kb,\n\t\tm.KeyMap.Quit,\n\t\tm.KeyMap.ShowFullHelp,\n\t)\n}\n\n// FullHelp returns bindings to show the full help view. It's part of the\n// help.KeyMap interface.\nfunc (m Model) FullHelp() [][]key.Binding {\n\tkb := [][]key.Binding{{\n\t\tm.KeyMap.CursorUp,\n\t\tm.KeyMap.CursorDown,\n\t\tm.KeyMap.NextPage,\n\t\tm.KeyMap.PrevPage,\n\t\tm.KeyMap.GoToStart,\n\t\tm.KeyMap.GoToEnd,\n\t}}\n\n\tfiltering := m.filterState == Filtering\n\n\t// If the delegate implements the help.KeyMap interface add full help\n\t// keybindings to a special section of the full help.\n\tif !filtering {\n\t\tif b, ok := m.delegate.(help.KeyMap); ok {\n\t\t\tkb = append(kb, b.FullHelp()...)\n\t\t}\n\t}\n\n\tlistLevelBindings := []key.Binding{\n\t\tm.KeyMap.Filter,\n\t\tm.KeyMap.ClearFilter,\n\t\tm.KeyMap.AcceptWhileFiltering,\n\t\tm.KeyMap.CancelWhileFiltering,\n\t}\n\n\tif !filtering && m.AdditionalFullHelpKeys != nil {\n\t\tlistLevelBindings = append(listLevelBindings, m.AdditionalFullHelpKeys()...)\n\t}\n\n\treturn append(kb,\n\t\tlistLevelBindings,\n\t\t[]key.Binding{\n\t\t\tm.KeyMap.Quit,\n\t\t\tm.KeyMap.CloseFullHelp,\n\t\t})\n}\n\n// View renders the component.\nfunc (m Model) View() string {\n\tvar (\n\t\tsections    []string\n\t\tavailHeight = m.height\n\t)\n\n\tif m.showTitle || (m.showFilter && m.filteringEnabled) {\n\t\tv := m.titleView()\n\t\tsections = append(sections, v)\n\t\tavailHeight -= lipgloss.Height(v)\n\t}\n\n\tif m.showStatusBar {\n\t\tv := m.statusView()\n\t\tsections = append(sections, v)\n\t\tavailHeight -= lipgloss.Height(v)\n\t}\n\n\tvar pagination string\n\tif m.showPagination {\n\t\tpagination = m.paginationView()\n\t\tavailHeight -= lipgloss.Height(pagination)\n\t}\n\n\tvar help string\n\tif m.showHelp {\n\t\thelp = m.helpView()\n\t\tavailHeight -= lipgloss.Height(help)\n\t}\n\n\tcontent := lipgloss.NewStyle().Height(availHeight).Render(m.populatedView())\n\tsections = append(sections, content)\n\n\tif m.showPagination {\n\t\tsections = append(sections, pagination)\n\t}\n\n\tif m.showHelp {\n\t\tsections = append(sections, help)\n\t}\n\n\treturn lipgloss.JoinVertical(lipgloss.Left, sections...)\n}\n\nfunc (m Model) titleView() string {\n\tvar (\n\t\tview          string\n\t\ttitleBarStyle = m.Styles.TitleBar\n\n\t\t// We need to account for the size of the spinner, even if we don't\n\t\t// render it, to reserve some space for it should we turn it on later.\n\t\tspinnerView    = m.spinnerView()\n\t\tspinnerWidth   = lipgloss.Width(spinnerView)\n\t\tspinnerLeftGap = \" \"\n\t\tspinnerOnLeft  = titleBarStyle.GetPaddingLeft() >= spinnerWidth+lipgloss.Width(spinnerLeftGap) && m.showSpinner\n\t)\n\n\t// If the filter's showing, draw that. Otherwise draw the title.\n\tif m.showFilter && m.filterState == Filtering {\n\t\tview += m.FilterInput.View()\n\t} else if m.showTitle {\n\t\tif m.showSpinner && spinnerOnLeft {\n\t\t\tview += spinnerView + spinnerLeftGap\n\t\t\ttitleBarGap := titleBarStyle.GetPaddingLeft()\n\t\t\ttitleBarStyle = titleBarStyle.PaddingLeft(titleBarGap - spinnerWidth - lipgloss.Width(spinnerLeftGap))\n\t\t}\n\n\t\tview += m.Styles.Title.Render(m.Title)\n\n\t\t// Status message\n\t\tif m.filterState != Filtering {\n\t\t\tview += \"  \" + m.statusMessage\n\t\t\tview = ansi.Truncate(view, m.width-spinnerWidth, ellipsis)\n\t\t}\n\t}\n\n\t// Spinner\n\tif m.showSpinner && !spinnerOnLeft {\n\t\t// Place spinner on the right\n\t\tavailSpace := m.width - lipgloss.Width(m.Styles.TitleBar.Render(view))\n\t\tif availSpace > spinnerWidth {\n\t\t\tview += strings.Repeat(\" \", availSpace-spinnerWidth)\n\t\t\tview += spinnerView\n\t\t}\n\t}\n\n\tif len(view) > 0 {\n\t\treturn titleBarStyle.Render(view)\n\t}\n\treturn view\n}\n\nfunc (m Model) statusView() string {\n\tvar status string\n\n\ttotalItems := len(m.items)\n\tvisibleItems := len(m.VisibleItems())\n\n\tvar itemName string\n\tif visibleItems != 1 {\n\t\titemName = m.itemNamePlural\n\t} else {\n\t\titemName = m.itemNameSingular\n\t}\n\n\titemsDisplay := fmt.Sprintf(\"%d %s\", visibleItems, itemName)\n\n\tif m.filterState == Filtering { //nolint:nestif\n\t\t// Filter results\n\t\tif visibleItems == 0 {\n\t\t\tstatus = m.Styles.StatusEmpty.Render(\"Nothing matched\")\n\t\t} else {\n\t\t\tstatus = itemsDisplay\n\t\t}\n\t} else if len(m.items) == 0 {\n\t\t// Not filtering: no items.\n\t\tstatus = m.Styles.StatusEmpty.Render(\"No \" + m.itemNamePlural)\n\t} else {\n\t\t// Normal\n\t\tfiltered := m.FilterState() == FilterApplied\n\n\t\tif filtered {\n\t\t\tf := strings.TrimSpace(m.FilterInput.Value())\n\t\t\tf = ansi.Truncate(f, 10, \"…\") //nolint:mnd\n\t\t\tstatus += fmt.Sprintf(\"“%s” \", f)\n\t\t}\n\n\t\tstatus += itemsDisplay\n\t}\n\n\tnumFiltered := totalItems - visibleItems\n\tif numFiltered > 0 {\n\t\tstatus += m.Styles.DividerDot.String()\n\t\tstatus += m.Styles.StatusBarFilterCount.Render(fmt.Sprintf(\"%d filtered\", numFiltered))\n\t}\n\n\treturn m.Styles.StatusBar.Render(status)\n}\n\nfunc (m Model) paginationView() string {\n\tif m.Paginator.TotalPages < 2 { //nolint:mnd\n\t\treturn \"\"\n\t}\n\n\ts := m.Paginator.View()\n\n\t// If the dot pagination is wider than the width of the window\n\t// use the arabic paginator.\n\tif ansi.StringWidth(s) > m.width {\n\t\tm.Paginator.Type = paginator.Arabic\n\t\ts = m.Styles.ArabicPagination.Render(m.Paginator.View())\n\t}\n\n\tstyle := m.Styles.PaginationStyle\n\tif m.delegate.Spacing() == 0 && style.GetMarginTop() == 0 {\n\t\tstyle = style.MarginTop(1)\n\t}\n\n\treturn style.Render(s)\n}\n\nfunc (m Model) populatedView() string {\n\titems := m.VisibleItems()\n\n\tvar b strings.Builder\n\n\t// Empty states\n\tif len(items) == 0 {\n\t\tif m.filterState == Filtering {\n\t\t\treturn \"\"\n\t\t}\n\t\treturn m.Styles.NoItems.Render(\"No \" + m.itemNamePlural + \".\")\n\t}\n\n\tif len(items) > 0 {\n\t\tstart, end := m.Paginator.GetSliceBounds(len(items))\n\t\tdocs := items[start:end]\n\n\t\tfor i, item := range docs {\n\t\t\tm.delegate.Render(&b, m, i+start, item)\n\t\t\tif i != len(docs)-1 {\n\t\t\t\tfmt.Fprint(&b, strings.Repeat(\"\\n\", m.delegate.Spacing()+1))\n\t\t\t}\n\t\t}\n\t}\n\n\t// If there aren't enough items to fill up this page (always the last page)\n\t// then we need to add some newlines to fill up the space where items would\n\t// have been.\n\titemsOnPage := m.Paginator.ItemsOnPage(len(items))\n\tif itemsOnPage < m.Paginator.PerPage {\n\t\tn := (m.Paginator.PerPage - itemsOnPage) * (m.delegate.Height() + m.delegate.Spacing())\n\t\tif len(items) == 0 {\n\t\t\tn -= m.delegate.Height() - 1\n\t\t}\n\t\tfmt.Fprint(&b, strings.Repeat(\"\\n\", n))\n\t}\n\n\treturn b.String()\n}\n\nfunc (m Model) helpView() string {\n\treturn m.Styles.HelpStyle.Render(m.Help.View(m))\n}\n\nfunc (m Model) spinnerView() string {\n\treturn m.spinner.View()\n}\n\nfunc filterItems(m Model) tea.Cmd {\n\treturn func() tea.Msg {\n\t\tif m.FilterInput.Value() == \"\" || m.filterState == Unfiltered {\n\t\t\treturn FilterMatchesMsg(m.itemsAsFilterItems()) // return nothing\n\t\t}\n\n\t\titems := m.items\n\t\ttargets := make([]string, len(items))\n\n\t\tfor i, t := range items {\n\t\t\ttargets[i] = t.FilterValue()\n\t\t}\n\n\t\tfilterMatches := []filteredItem{}\n\t\tfor _, r := range m.Filter(m.FilterInput.Value(), targets) {\n\t\t\tfilterMatches = append(filterMatches, filteredItem{\n\t\t\t\tindex:   r.Index,\n\t\t\t\titem:    items[r.Index],\n\t\t\t\tmatches: r.MatchedIndexes,\n\t\t\t})\n\t\t}\n\n\t\treturn FilterMatchesMsg(filterMatches)\n\t}\n}\n\nfunc insertItemIntoSlice(items []Item, item Item, index int) []Item {\n\tif items == nil {\n\t\treturn []Item{item}\n\t}\n\tif index >= len(items) {\n\t\treturn append(items, item)\n\t}\n\n\tindex = max(0, index)\n\n\titems = append(items, nil)\n\tcopy(items[index+1:], items[index:])\n\titems[index] = item\n\treturn items\n}\n\n// Remove an item from a slice of items at the given index. This runs in O(n).\nfunc removeItemFromSlice(i []Item, index int) []Item {\n\tif index >= len(i) {\n\t\treturn i // noop\n\t}\n\tcopy(i[index:], i[index+1:])\n\ti[len(i)-1] = nil\n\treturn i[:len(i)-1]\n}\n\nfunc removeFilterMatchFromSlice(i []filteredItem, index int) []filteredItem {\n\tif index >= len(i) {\n\t\treturn i // noop\n\t}\n\tcopy(i[index:], i[index+1:])\n\ti[len(i)-1] = filteredItem{}\n\treturn i[:len(i)-1]\n}\n\nfunc countEnabledBindings(groups [][]key.Binding) (agg int) {\n\tfor _, group := range groups {\n\t\tfor _, kb := range group {\n\t\t\tif kb.Enabled() {\n\t\t\t\tagg++\n\t\t\t}\n\t\t}\n\t}\n\treturn agg\n}\n"
  },
  {
    "path": "list/list_test.go",
    "content": "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 item string\n\nfunc (i item) FilterValue() string { return string(i) }\n\ntype itemDelegate struct{}\n\nfunc (d itemDelegate) Height() int                          { return 1 }\nfunc (d itemDelegate) Spacing() int                         { return 0 }\nfunc (d itemDelegate) Update(msg tea.Msg, m *Model) tea.Cmd { return nil }\nfunc (d itemDelegate) Render(w io.Writer, m Model, index int, listItem Item) {\n\ti, ok := listItem.(item)\n\tif !ok {\n\t\treturn\n\t}\n\n\tstr := fmt.Sprintf(\"%d. %s\", index+1, i)\n\tfmt.Fprint(w, m.Styles.TitleBar.Render(str))\n}\n\nfunc TestStatusBarItemName(t *testing.T) {\n\tlist := New([]Item{item(\"foo\"), item(\"bar\")}, itemDelegate{}, 10, 10)\n\texpected := \"2 items\"\n\tif !strings.Contains(list.statusView(), expected) {\n\t\tt.Fatalf(\"Error: expected view to contain %s\", expected)\n\t}\n\n\tlist.SetItems([]Item{item(\"foo\")})\n\texpected = \"1 item\"\n\tif !strings.Contains(list.statusView(), expected) {\n\t\tt.Fatalf(\"Error: expected view to contain %s\", expected)\n\t}\n}\n\nfunc TestStatusBarWithoutItems(t *testing.T) {\n\tlist := New([]Item{}, itemDelegate{}, 10, 10)\n\n\texpected := \"No items\"\n\tif !strings.Contains(list.statusView(), expected) {\n\t\tt.Fatalf(\"Error: expected view to contain %s\", expected)\n\t}\n}\n\nfunc TestCustomStatusBarItemName(t *testing.T) {\n\tlist := New([]Item{item(\"foo\"), item(\"bar\")}, itemDelegate{}, 10, 10)\n\tlist.SetStatusBarItemName(\"connection\", \"connections\")\n\n\texpected := \"2 connections\"\n\tif !strings.Contains(list.statusView(), expected) {\n\t\tt.Fatalf(\"Error: expected view to contain %s\", expected)\n\t}\n\n\tlist.SetItems([]Item{item(\"foo\")})\n\texpected = \"1 connection\"\n\tif !strings.Contains(list.statusView(), expected) {\n\t\tt.Fatalf(\"Error: expected view to contain %s\", expected)\n\t}\n\n\tlist.SetItems([]Item{})\n\texpected = \"No connections\"\n\tif !strings.Contains(list.statusView(), expected) {\n\t\tt.Fatalf(\"Error: expected view to contain %s\", expected)\n\t}\n}\n\nfunc TestSetFilterText(t *testing.T) {\n\ttc := []Item{item(\"foo\"), item(\"bar\"), item(\"baz\")}\n\n\tlist := New(tc, itemDelegate{}, 10, 10)\n\tlist.SetFilterText(\"ba\")\n\n\tlist.SetFilterState(Unfiltered)\n\texpected := tc\n\tif !slices.Equal(list.VisibleItems(), expected) {\n\t\tt.Fatalf(\"Error: expected view to contain only %s\", expected)\n\t}\n\n\tlist.SetFilterState(Filtering)\n\texpected = []Item{item(\"bar\"), item(\"baz\")}\n\tif !reflect.DeepEqual(list.VisibleItems(), expected) {\n\t\tt.Fatalf(\"Error: expected view to contain only %s\", expected)\n\t}\n\n\tlist.SetFilterState(FilterApplied)\n\tif !reflect.DeepEqual(list.VisibleItems(), expected) {\n\t\tt.Fatalf(\"Error: expected view to contain only %s\", expected)\n\t}\n}\n\nfunc TestSetFilterState(t *testing.T) {\n\ttc := []Item{item(\"foo\"), item(\"bar\"), item(\"baz\")}\n\n\tlist := New(tc, itemDelegate{}, 10, 10)\n\tlist.SetFilterText(\"ba\")\n\n\tlist.SetFilterState(Unfiltered)\n\texpected, notExpected := \"up\", \"clear filter\"\n\n\tlines := strings.Split(list.View(), \"\\n\")\n\tfooter := lines[len(lines)-1]\n\n\tif !strings.Contains(footer, expected) || strings.Contains(footer, notExpected) {\n\t\tt.Fatalf(\"Error: expected view to contain '%s' not '%s'\", expected, notExpected)\n\t}\n\n\tlist.SetFilterState(Filtering)\n\texpected, notExpected = \"filter\", \"more\"\n\n\tlines = strings.Split(list.View(), \"\\n\")\n\tfooter = lines[len(lines)-1]\n\n\tif !strings.Contains(footer, expected) || strings.Contains(footer, notExpected) {\n\t\tt.Fatalf(\"Error: expected view to contain '%s' not '%s'\", expected, notExpected)\n\t}\n\n\tlist.SetFilterState(FilterApplied)\n\texpected = \"clear\"\n\n\tlines = strings.Split(list.View(), \"\\n\")\n\tfooter = lines[len(lines)-1]\n\n\tif !strings.Contains(footer, expected) {\n\t\tt.Fatalf(\"Error: expected view to contain '%s'\", expected)\n\t}\n}\n"
  },
  {
    "path": "list/style.go",
    "content": "package list\n\nimport (\n\t\"charm.land/bubbles/v2/textinput\"\n\t\"charm.land/lipgloss/v2\"\n)\n\nconst (\n\tbullet   = \"•\"\n\tellipsis = \"…\"\n)\n\n// Styles contains style definitions for this list component. By default, these\n// values are generated by DefaultStyles.\ntype Styles struct {\n\tTitleBar lipgloss.Style\n\tTitle    lipgloss.Style\n\tSpinner  lipgloss.Style\n\tFilter   textinput.Styles\n\n\t// Default styling for matched characters in a filter. This can be\n\t// overridden by delegates.\n\tDefaultFilterCharacterMatch lipgloss.Style\n\n\tStatusBar             lipgloss.Style\n\tStatusEmpty           lipgloss.Style\n\tStatusBarActiveFilter lipgloss.Style\n\tStatusBarFilterCount  lipgloss.Style\n\n\tNoItems lipgloss.Style\n\n\tPaginationStyle lipgloss.Style\n\tHelpStyle       lipgloss.Style\n\n\t// Styled characters.\n\tActivePaginationDot   lipgloss.Style\n\tInactivePaginationDot lipgloss.Style\n\tArabicPagination      lipgloss.Style\n\tDividerDot            lipgloss.Style\n}\n\n// DefaultStyles returns a set of default style definitions for this list\n// component.\nfunc DefaultStyles(isDark bool) (s Styles) {\n\tlightDark := lipgloss.LightDark(isDark)\n\n\tverySubduedColor := lightDark(lipgloss.Color(\"#DDDADA\"), lipgloss.Color(\"#3C3C3C\"))\n\tsubduedColor := lightDark(lipgloss.Color(\"#9B9B9B\"), lipgloss.Color(\"#5C5C5C\"))\n\n\ts.TitleBar = lipgloss.NewStyle().Padding(0, 0, 1, 2) //nolint:mnd\n\n\ts.Title = lipgloss.NewStyle().\n\t\tBackground(lipgloss.Color(\"62\")).\n\t\tForeground(lipgloss.Color(\"230\")).\n\t\tPadding(0, 1)\n\n\ts.Spinner = lipgloss.NewStyle().\n\t\tForeground(lightDark(lipgloss.Color(\"#8E8E8E\"), lipgloss.Color(\"#747373\")))\n\n\tprompt := lipgloss.NewStyle().\n\t\tForeground(lightDark(lipgloss.Color(\"#04B575\"), lipgloss.Color(\"#ECFD65\")))\n\ts.Filter = textinput.DefaultStyles(isDark)\n\ts.Filter.Cursor.Color = lightDark(lipgloss.Color(\"#EE6FF8\"), lipgloss.Color(\"#EE6FF8\"))\n\ts.Filter.Blurred.Prompt = prompt\n\ts.Filter.Focused.Prompt = prompt\n\n\ts.DefaultFilterCharacterMatch = lipgloss.NewStyle().Underline(true)\n\n\ts.StatusBar = lipgloss.NewStyle().\n\t\tForeground(lightDark(lipgloss.Color(\"#A49FA5\"), lipgloss.Color(\"#777777\"))).\n\t\tPadding(0, 0, 1, 2) //nolint:mnd\n\n\ts.StatusEmpty = lipgloss.NewStyle().Foreground(subduedColor)\n\n\ts.StatusBarActiveFilter = lipgloss.NewStyle().\n\t\tForeground(lightDark(lipgloss.Color(\"#1a1a1a\"), lipgloss.Color(\"#dddddd\")))\n\n\ts.StatusBarFilterCount = lipgloss.NewStyle().Foreground(verySubduedColor)\n\n\ts.NoItems = lipgloss.NewStyle().\n\t\tForeground(lightDark(lipgloss.Color(\"#909090\"), lipgloss.Color(\"#626262\")))\n\n\ts.ArabicPagination = lipgloss.NewStyle().Foreground(subduedColor)\n\n\ts.PaginationStyle = lipgloss.NewStyle().PaddingLeft(2) //nolint:mnd\n\n\ts.HelpStyle = lipgloss.NewStyle().Padding(1, 0, 0, 2) //nolint:mnd\n\n\ts.ActivePaginationDot = lipgloss.NewStyle().\n\t\tForeground(lightDark(lipgloss.Color(\"#847A85\"), lipgloss.Color(\"#979797\"))).\n\t\tSetString(bullet)\n\n\ts.InactivePaginationDot = lipgloss.NewStyle().\n\t\tForeground(verySubduedColor).\n\t\tSetString(bullet)\n\n\ts.DividerDot = lipgloss.NewStyle().\n\t\tForeground(verySubduedColor).\n\t\tSetString(\" \" + bullet + \" \")\n\n\treturn s\n}\n"
  },
  {
    "path": "paginator/paginator.go",
    "content": "// Package paginator provides a Bubble Tea package for calculating pagination\n// and rendering pagination info. Note that this package does not render actual\n// pages: it's purely for handling keystrokes related to pagination, and\n// rendering pagination status.\npackage paginator\n\nimport (\n\t\"fmt\"\n\n\t\"charm.land/bubbles/v2/key\"\n\ttea \"charm.land/bubbletea/v2\"\n)\n\n// Type specifies the way we render pagination.\ntype Type int\n\n// Pagination rendering options.\nconst (\n\tArabic Type = iota\n\tDots\n)\n\n// KeyMap is the key bindings for different actions within the paginator.\ntype KeyMap struct {\n\tPrevPage key.Binding\n\tNextPage key.Binding\n}\n\n// DefaultKeyMap is the default set of key bindings for navigating and acting\n// upon the paginator.\nfunc DefaultKeyMap() KeyMap {\n\treturn KeyMap{\n\t\tPrevPage: key.NewBinding(key.WithKeys(\"pgup\", \"left\", \"h\")),\n\t\tNextPage: key.NewBinding(key.WithKeys(\"pgdown\", \"right\", \"l\")),\n\t}\n}\n\n// Model is the Bubble Tea model for this user interface.\ntype Model struct {\n\t// Type configures how the pagination is rendered (Arabic, Dots).\n\tType Type\n\t// Page is the current page number.\n\tPage int\n\t// PerPage is the number of items per page.\n\tPerPage int\n\t// TotalPages is the total number of pages.\n\tTotalPages int\n\t// ActiveDot is used to mark the current page under the Dots display type.\n\tActiveDot string\n\t// InactiveDot is used to mark inactive pages under the Dots display type.\n\tInactiveDot string\n\t// ArabicFormat is the printf-style format to use for the Arabic display type.\n\tArabicFormat string\n\n\t// KeyMap encodes the keybindings recognized by the widget.\n\tKeyMap KeyMap\n}\n\n// SetTotalPages is a helper function for calculating the total number of pages\n// from a given number of items. Its use is optional since this pager can be\n// used for other things beyond navigating sets. Note that it both returns the\n// number of total pages and alters the model.\nfunc (m *Model) SetTotalPages(items int) int {\n\tif items < 1 {\n\t\treturn m.TotalPages\n\t}\n\tn := items / m.PerPage\n\tif items%m.PerPage > 0 {\n\t\tn++\n\t}\n\tm.TotalPages = n\n\treturn n\n}\n\n// ItemsOnPage is a helper function for returning the number of items on the\n// current page given the total number of items passed as an argument.\nfunc (m Model) ItemsOnPage(totalItems int) int {\n\tif totalItems < 1 {\n\t\treturn 0\n\t}\n\tstart, end := m.GetSliceBounds(totalItems)\n\treturn end - start\n}\n\n// GetSliceBounds is a helper function for paginating slices. Pass the length\n// of the slice you're rendering and you'll receive the start and end bounds\n// corresponding to the pagination. For example:\n//\n//\tbunchOfStuff := []stuff{...}\n//\tstart, end := model.GetSliceBounds(len(bunchOfStuff))\n//\tsliceToRender := bunchOfStuff[start:end]\nfunc (m *Model) GetSliceBounds(length int) (start int, end int) {\n\tstart = m.Page * m.PerPage\n\tend = min(m.Page*m.PerPage+m.PerPage, length)\n\treturn start, end\n}\n\n// PrevPage is a helper function for navigating one page backward. It will not\n// page beyond the first page (i.e. page 0).\nfunc (m *Model) PrevPage() {\n\tif m.Page > 0 {\n\t\tm.Page--\n\t}\n}\n\n// NextPage is a helper function for navigating one page forward. It will not\n// page beyond the last page (i.e. totalPages - 1).\nfunc (m *Model) NextPage() {\n\tif !m.OnLastPage() {\n\t\tm.Page++\n\t}\n}\n\n// OnLastPage returns whether or not we're on the last page.\nfunc (m Model) OnLastPage() bool {\n\treturn m.Page == m.TotalPages-1\n}\n\n// OnFirstPage returns whether or not we're on the first page.\nfunc (m Model) OnFirstPage() bool {\n\treturn m.Page == 0\n}\n\n// Option is used to set options in New.\ntype Option func(*Model)\n\n// New creates a new model with defaults.\nfunc New(opts ...Option) Model {\n\tm := Model{\n\t\tType:         Arabic,\n\t\tPage:         0,\n\t\tPerPage:      1,\n\t\tTotalPages:   1,\n\t\tKeyMap:       DefaultKeyMap(),\n\t\tActiveDot:    \"•\",\n\t\tInactiveDot:  \"○\",\n\t\tArabicFormat: \"%d/%d\",\n\t}\n\n\tfor _, opt := range opts {\n\t\topt(&m)\n\t}\n\n\treturn m\n}\n\n// WithTotalPages sets the total pages.\nfunc WithTotalPages(totalPages int) Option {\n\treturn func(m *Model) {\n\t\tm.TotalPages = totalPages\n\t}\n}\n\n// WithPerPage sets the total pages.\nfunc WithPerPage(perPage int) Option {\n\treturn func(m *Model) {\n\t\tm.PerPage = perPage\n\t}\n}\n\n// Update is the Tea update function which binds keystrokes to pagination.\nfunc (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {\n\tswitch msg := msg.(type) {\n\tcase tea.KeyPressMsg:\n\t\tswitch {\n\t\tcase key.Matches(msg, m.KeyMap.NextPage):\n\t\t\tm.NextPage()\n\t\tcase key.Matches(msg, m.KeyMap.PrevPage):\n\t\t\tm.PrevPage()\n\t\t}\n\t}\n\n\treturn m, nil\n}\n\n// View renders the pagination to a string.\nfunc (m Model) View() string {\n\tswitch m.Type { //nolint:exhaustive\n\tcase Dots:\n\t\treturn m.dotsView()\n\tdefault:\n\t\treturn m.arabicView()\n\t}\n}\n\nfunc (m Model) dotsView() string {\n\tvar s string\n\tfor i := range m.TotalPages {\n\t\tif i == m.Page {\n\t\t\ts += m.ActiveDot\n\t\t\tcontinue\n\t\t}\n\t\ts += m.InactiveDot\n\t}\n\treturn s\n}\n\nfunc (m Model) arabicView() string {\n\treturn fmt.Sprintf(m.ArabicFormat, m.Page+1, m.TotalPages)\n}\n"
  },
  {
    "path": "paginator/paginator_test.go",
    "content": "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\tif model.PerPage != 1 {\n\t\tt.Errorf(\"PerPage = %d, expected %d\", model.PerPage, 1)\n\t}\n\tif model.TotalPages != 1 {\n\t\tt.Errorf(\"TotalPages = %d, expected %d\", model.TotalPages, 1)\n\t}\n\n\tperPage := 42\n\ttotalPages := 42\n\n\tmodel = New(\n\t\tWithPerPage(perPage),\n\t\tWithTotalPages(totalPages),\n\t)\n\n\tif model.PerPage != perPage {\n\t\tt.Errorf(\"PerPage = %d, expected %d\", model.PerPage, perPage)\n\t}\n\tif model.TotalPages != totalPages {\n\t\tt.Errorf(\"TotalPages = %d, expected %d\", model.TotalPages, totalPages)\n\t}\n}\n\nfunc TestSetTotalPages(t *testing.T) {\n\ttests := []struct {\n\t\tname         string\n\t\titems        int // total no of items to be set\n\t\tinitialTotal int // intital total pages for the testcase\n\t\texpected     int // expected value after SetTotalPages function call\n\t}{\n\t\t{\"Less than one page\", 5, 1, 5},\n\t\t{\"Exactly one page\", 10, 1, 10},\n\t\t{\"More than one page\", 15, 1, 15},\n\t\t{\"negative value for page\", -10, 1, 1},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tmodel := New()\n\t\t\tif model.TotalPages != tt.initialTotal {\n\t\t\t\tmodel.SetTotalPages(tt.initialTotal)\n\t\t\t}\n\t\t\tmodel.SetTotalPages(tt.items)\n\t\t\tif model.TotalPages != tt.expected {\n\t\t\t\tt.Errorf(\"TotalPages = %d, expected %d\", model.TotalPages, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestPrevPage(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\ttotalPages int // Total pages to be set for the testcase\n\t\tpage       int // intital page for test\n\t\texpected   int\n\t}{\n\t\t{\"Go to previous page\", 10, 1, 0},\n\t\t{\"Stay on first page\", 5, 0, 0},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tmodel := New()\n\t\t\tmodel.SetTotalPages(tt.totalPages)\n\t\t\tmodel.Page = tt.page\n\n\t\t\tmodel, _ = model.Update(tea.KeyPressMsg{Code: tea.KeyLeft})\n\t\t\tif model.Page != tt.expected {\n\t\t\t\tt.Errorf(\"PrevPage() = %d, expected %d\", model.Page, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNextPage(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\ttotalPages int\n\t\tpage       int\n\t\texpected   int\n\t}{\n\t\t{\"Go to next page\", 2, 0, 1},\n\t\t{\"Stay on last page\", 2, 1, 1},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tmodel := New()\n\t\t\tmodel.SetTotalPages(tt.totalPages)\n\t\t\tmodel.Page = tt.page\n\n\t\t\tmodel, _ = model.Update(tea.KeyPressMsg{Code: tea.KeyRight})\n\t\t\tif model.Page != tt.expected {\n\t\t\t\tt.Errorf(\"NextPage() = %d, expected %d\", model.Page, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestOnLastPage(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\tpage       int\n\t\ttotalPages int\n\t\texpected   bool\n\t}{\n\t\t{\"On last page\", 1, 2, true},\n\t\t{\"Not on last page\", 0, 2, false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tmodel := New()\n\t\t\tmodel.SetTotalPages(tt.totalPages)\n\t\t\tmodel.Page = tt.page\n\n\t\t\tif result := model.OnLastPage(); result != tt.expected {\n\t\t\t\tt.Errorf(\"OnLastPage() = %t, expected %t\", result, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestOnFirstPage(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\tpage       int\n\t\ttotalPages int\n\t\texpected   bool\n\t}{\n\t\t{\"On first page\", 0, 2, true},\n\t\t{\"Not on first page\", 1, 2, false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tmodel := New()\n\t\t\tmodel.SetTotalPages(tt.totalPages)\n\t\t\tmodel.Page = tt.page\n\n\t\t\tif result := model.OnFirstPage(); result != tt.expected {\n\t\t\t\tt.Errorf(\"OnFirstPage() = %t, expected %t\", result, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestItemsOnPage(t *testing.T) {\n\ttestCases := []struct {\n\t\tcurrentPage   int // current page to be set for the testcase\n\t\ttotalPages    int // Total pages to be set for the testcase\n\t\ttotalItems    int // Total items\n\t\texpectedItems int // expected items on current page\n\t}{\n\t\t{1, 10, 10, 1},\n\t\t{3, 10, 10, 1},\n\t\t{7, 10, 10, 1},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tmodel := New()\n\t\tmodel.Page = tc.currentPage\n\t\tmodel.SetTotalPages(tc.totalPages)\n\t\tif actualItems := model.ItemsOnPage(tc.totalItems); actualItems != tc.expectedItems {\n\t\t\tt.Errorf(\"ItemsOnPage() returned %d, expected %d for total items %d\", actualItems, tc.expectedItems, tc.totalItems)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "progress/progress.go",
    "content": "// Package progress provides a simple progress bar for Bubble Tea applications.\npackage progress\n\nimport (\n\t\"fmt\"\n\t\"image/color\"\n\t\"math\"\n\t\"strings\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\ttea \"charm.land/bubbletea/v2\"\n\t\"charm.land/lipgloss/v2\"\n\t\"github.com/charmbracelet/harmonica\"\n\t\"github.com/charmbracelet/x/ansi\"\n)\n\n// ColorFunc is a function that can be used to dynamically fill the progress\n// bar based on the current percentage. total is the total filled percentage,\n// and current is the current percentage that is actively being filled with a\n// color.\ntype ColorFunc func(total, current float64) color.Color\n\n// Internal ID management. Used during animating to assure that frame messages\n// can only be received by progress components that sent them.\nvar lastID int64\n\nfunc nextID() int {\n\treturn int(atomic.AddInt64(&lastID, 1))\n}\n\nconst (\n\t// DefaultFullCharHalfBlock is the default character used to fill the progress\n\t// bar. It is a half block, which allows more granular color blending control,\n\t// by having a different foreground and background color, doubling blending\n\t// resolution.\n\tDefaultFullCharHalfBlock = '▌'\n\n\t// DefaultFullCharFullBlock can also be used as a fill character for the\n\t// progress bar. Use this to disable the higher resolution blending which is\n\t// enabled when using [DefaultFullCharHalfBlock].\n\tDefaultFullCharFullBlock = '█'\n\n\t// DefaultEmptyCharBlock is the default character used to fill the empty\n\t// portion of the progress bar.\n\tDefaultEmptyCharBlock = '░'\n\n\tfps              = 60\n\tdefaultWidth     = 40\n\tdefaultFrequency = 18.0\n\tdefaultDamping   = 1.0\n)\n\nvar (\n\tdefaultBlendStart = lipgloss.Color(\"#5A56E0\") // Purple haze.\n\tdefaultBlendEnd   = lipgloss.Color(\"#EE6FF8\") // Neon pink.\n\tdefaultFullColor  = lipgloss.Color(\"#7571F9\") // Blueberry.\n\tdefaultEmptyColor = lipgloss.Color(\"#606060\") // Slate gray.\n)\n\n// Option is used to set options in [New]. For example:\n//\n//\tprogress := New(\n//\t\tWithColors(\n//\t\t\tlipgloss.Color(\"#5A56E0\"),\n//\t\t\tlipgloss.Color(\"#EE6FF8\"),\n//\t\t),\n//\t\tWithoutPercentage(),\n//\t)\ntype Option func(*Model)\n\n// WithDefaultBlend sets a default blend of colors, which is a blend of purple\n// haze to neon pink.\nfunc WithDefaultBlend() Option {\n\treturn WithColors(\n\t\tdefaultBlendStart,\n\t\tdefaultBlendEnd,\n\t)\n}\n\n// WithColors sets the colors to use to fill the progress bar. Depending on the\n// number of colors passed in, will determine whether to use a solid fill or a\n// blend of colors.\n//\n//   - 0 colors: clears all previously set colors, setting them back to defaults.\n//   - 1 color: uses a solid fill with the given color.\n//   - 2+ colors: uses a blend of the provided colors.\nfunc WithColors(colors ...color.Color) Option {\n\tif len(colors) == 0 {\n\t\treturn func(m *Model) {\n\t\t\tm.FullColor = defaultFullColor\n\t\t\tm.blend = nil\n\t\t\tm.colorFunc = nil\n\t\t}\n\t}\n\tif len(colors) == 1 {\n\t\treturn func(m *Model) {\n\t\t\tm.FullColor = colors[0]\n\t\t\tm.colorFunc = nil\n\t\t\tm.blend = nil\n\t\t}\n\t}\n\treturn func(m *Model) {\n\t\tm.blend = colors\n\t}\n}\n\n// WithColorFunc sets a function that can be used to dynamically fill the progress\n// bar based on the current percentage. total is the total filled percentage, and\n// current is the current percentage that is actively being filled with a color.\n// When specified, this overrides any other defined colors and scaling.\n//\n// Example: A progress bar that changes color based on the total completed\n// percentage:\n//\n//\tWithColorFunc(func(total, current float64) color.Color {\n//\t\tif total <= 0.3 {\n//\t\t\treturn lipgloss.Color(\"#FF0000\")\n//\t\t}\n//\t\tif total <= 0.7 {\n//\t\t\treturn lipgloss.Color(\"#00FF00\")\n//\t\t}\n//\t\treturn lipgloss.Color(\"#0000FF\")\n//\t}),\nfunc WithColorFunc(fn ColorFunc) Option {\n\treturn func(m *Model) {\n\t\tm.colorFunc = fn\n\t\tm.blend = nil\n\t}\n}\n\n// WithFillCharacters sets the characters used to construct the full and empty\n// components of the progress bar.\nfunc WithFillCharacters(full rune, empty rune) Option {\n\treturn func(m *Model) {\n\t\tm.Full = full\n\t\tm.Empty = empty\n\t}\n}\n\n// WithoutPercentage hides the numeric percentage.\nfunc WithoutPercentage() Option {\n\treturn func(m *Model) {\n\t\tm.ShowPercentage = false\n\t}\n}\n\n// WithWidth sets the initial width of the progress bar. Note that you can also\n// set the width via the Width property, which can come in handy if you're\n// waiting for a tea.WindowSizeMsg.\nfunc WithWidth(w int) Option {\n\treturn func(m *Model) {\n\t\tm.SetWidth(w)\n\t}\n}\n\n// WithSpringOptions sets the initial frequency and damping options for the\n// progress bar's built-in spring-based animation. Frequency corresponds to\n// speed, and damping to bounciness. For details see:\n//\n// https://github.com/charmbracelet/harmonica\nfunc WithSpringOptions(frequency, damping float64) Option {\n\treturn func(m *Model) {\n\t\tm.SetSpringOptions(frequency, damping)\n\t\tm.springCustomized = true\n\t}\n}\n\n// WithScaled sets whether to scale the blend/gradient to fit the width of only\n// the filled portion of the progress bar. The default is false, which means the\n// percentage must be 100% to see the full color blend/gradient.\n//\n// This is ignored when not using blending/multiple colors.\nfunc WithScaled(enabled bool) Option {\n\treturn func(m *Model) {\n\t\tm.scaleBlend = enabled\n\t}\n}\n\n// FrameMsg indicates that an animation step should occur.\ntype FrameMsg struct {\n\tid  int\n\ttag int\n}\n\n// Model stores values we'll use when rendering the progress bar.\ntype Model struct {\n\t// An identifier to keep us from receiving messages intended for other\n\t// progress bars.\n\tid int\n\n\t// An identifier to keep us from receiving frame messages too quickly.\n\ttag int\n\n\t// Total width of the progress bar, including percentage, if set.\n\twidth int\n\n\t// \"Filled\" sections of the progress bar.\n\tFull      rune\n\tFullColor color.Color\n\n\t// \"Empty\" sections of the progress bar.\n\tEmpty      rune\n\tEmptyColor color.Color\n\n\t// Settings for rendering the numeric percentage.\n\tShowPercentage  bool\n\tPercentFormat   string // a fmt string for a float\n\tPercentageStyle lipgloss.Style\n\n\t// Members for animated transitions.\n\tspring           harmonica.Spring\n\tspringCustomized bool\n\tpercentShown     float64 // percent currently displaying\n\ttargetPercent    float64 // percent to which we're animating\n\tvelocity         float64\n\n\t// Blend of colors to use. When len < 1, we use FullColor.\n\tblend []color.Color\n\n\t// When true, we scale the blended colors to fit the width of the filled\n\t// section of the progress bar. When false, the width of the blend will be\n\t// set to the full width of the progress bar.\n\tscaleBlend bool\n\n\t// colorFunc is used to dynamically fill the progress bar based on the\n\t// current percentage.\n\tcolorFunc ColorFunc\n}\n\n// New returns a model with default values.\nfunc New(opts ...Option) Model {\n\tm := Model{\n\t\tid:             nextID(),\n\t\twidth:          defaultWidth,\n\t\tFull:           DefaultFullCharHalfBlock,\n\t\tFullColor:      defaultFullColor,\n\t\tEmpty:          DefaultEmptyCharBlock,\n\t\tEmptyColor:     defaultEmptyColor,\n\t\tShowPercentage: true,\n\t\tPercentFormat:  \" %3.0f%%\",\n\t}\n\n\tfor _, opt := range opts {\n\t\topt(&m)\n\t}\n\n\tif !m.springCustomized {\n\t\tm.SetSpringOptions(defaultFrequency, defaultDamping)\n\t}\n\n\treturn m\n}\n\n// Init exists to satisfy the tea.Model interface.\nfunc (m Model) Init() tea.Cmd {\n\treturn nil\n}\n\n// Update is used to animate the progress bar during transitions. Use\n// SetPercent to create the command you'll need to trigger the animation.\n//\n// If you're rendering with ViewAs you won't need this.\nfunc (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {\n\tswitch msg := msg.(type) {\n\tcase FrameMsg:\n\t\tif msg.id != m.id || msg.tag != m.tag {\n\t\t\treturn m, nil\n\t\t}\n\n\t\t// If we've more or less reached equilibrium, stop updating.\n\t\tif !m.IsAnimating() {\n\t\t\treturn m, nil\n\t\t}\n\n\t\tm.percentShown, m.velocity = m.spring.Update(m.percentShown, m.velocity, m.targetPercent)\n\t\treturn m, m.nextFrame()\n\n\tdefault:\n\t\treturn m, nil\n\t}\n}\n\n// SetSpringOptions sets the frequency and damping for the current spring.\n// Frequency corresponds to speed, and damping to bounciness. For details see:\n//\n// https://github.com/charmbracelet/harmonica\nfunc (m *Model) SetSpringOptions(frequency, damping float64) {\n\tm.spring = harmonica.NewSpring(harmonica.FPS(fps), frequency, damping)\n}\n\n// Percent returns the current visible percentage on the model. This is only\n// relevant when you're animating the progress bar.\n//\n// If you're rendering with ViewAs you won't need this.\nfunc (m Model) Percent() float64 {\n\treturn m.targetPercent\n}\n\n// SetPercent sets the percentage state of the model as well as a command\n// necessary for animating the progress bar to this new percentage.\n//\n// If you're rendering with ViewAs you won't need this.\nfunc (m *Model) SetPercent(p float64) tea.Cmd {\n\tm.targetPercent = math.Max(0, math.Min(1, p))\n\tm.tag++\n\treturn m.nextFrame()\n}\n\n// IncrPercent increments the percentage by a given amount, returning a command\n// necessary to animate the progress bar to the new percentage.\n//\n// If you're rendering with ViewAs you won't need this.\nfunc (m *Model) IncrPercent(v float64) tea.Cmd {\n\treturn m.SetPercent(m.Percent() + v)\n}\n\n// DecrPercent decrements the percentage by a given amount, returning a command\n// necessary to animate the progress bar to the new percentage.\n//\n// If you're rendering with ViewAs you won't need this.\nfunc (m *Model) DecrPercent(v float64) tea.Cmd {\n\treturn m.SetPercent(m.Percent() - v)\n}\n\n// View renders an animated progress bar in its current state. To render\n// a static progress bar based on your own calculations use ViewAs instead.\nfunc (m Model) View() string {\n\treturn m.ViewAs(m.percentShown)\n}\n\n// ViewAs renders the progress bar with a given percentage.\nfunc (m Model) ViewAs(percent float64) string {\n\tb := strings.Builder{}\n\tpercentView := m.percentageView(percent)\n\tm.barView(&b, percent, ansi.StringWidth(percentView))\n\tb.WriteString(percentView)\n\treturn b.String()\n}\n\n// SetWidth sets the width of the progress bar.\nfunc (m *Model) SetWidth(w int) {\n\tm.width = w\n}\n\n// Width returns the width of the progress bar.\nfunc (m Model) Width() int {\n\treturn m.width\n}\n\nfunc (m *Model) nextFrame() tea.Cmd {\n\treturn tea.Tick(time.Second/time.Duration(fps), func(time.Time) tea.Msg {\n\t\treturn FrameMsg{id: m.id, tag: m.tag}\n\t})\n}\n\nfunc (m Model) barView(b *strings.Builder, percent float64, textWidth int) {\n\tvar (\n\t\ttw = max(0, m.width-textWidth)                // total width\n\t\tfw = int(math.Round((float64(tw) * percent))) // filled width\n\t)\n\n\tfw = max(0, min(tw, fw))\n\n\tisHalfBlock := m.Full == DefaultFullCharHalfBlock\n\n\tif m.colorFunc != nil { //nolint:nestif\n\t\tvar style lipgloss.Style\n\t\tvar current float64\n\t\thalfBlockPerc := 0.5 / float64(tw)\n\t\tfor i := range fw {\n\t\t\tcurrent = float64(i) / float64(tw)\n\t\t\tstyle = style.Foreground(m.colorFunc(percent, current))\n\t\t\tif isHalfBlock {\n\t\t\t\tstyle = style.Background(m.colorFunc(percent, min(current+halfBlockPerc, 1)))\n\t\t\t}\n\t\t\tb.WriteString(style.Render(string(m.Full)))\n\t\t}\n\t} else if len(m.blend) > 0 {\n\t\tvar blend []color.Color\n\n\t\tmultiplier := 1\n\t\tif isHalfBlock {\n\t\t\tmultiplier = 2\n\t\t}\n\n\t\tif m.scaleBlend {\n\t\t\tblend = lipgloss.Blend1D(fw*multiplier, m.blend...)\n\t\t} else {\n\t\t\tblend = lipgloss.Blend1D(tw*multiplier, m.blend...)\n\t\t}\n\n\t\t// Blend fill.\n\t\tvar blendIndex int\n\t\tfor i := range fw {\n\t\t\tif !isHalfBlock {\n\t\t\t\tb.WriteString(lipgloss.NewStyle().\n\t\t\t\t\tForeground(blend[i]).\n\t\t\t\t\tRender(string(m.Full)))\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tb.WriteString(lipgloss.NewStyle().\n\t\t\t\tForeground(blend[blendIndex]).\n\t\t\t\tBackground(blend[blendIndex+1]).\n\t\t\t\tRender(string(m.Full)))\n\t\t\tblendIndex += 2\n\t\t}\n\t} else {\n\t\t// Solid fill.\n\t\tb.WriteString(lipgloss.NewStyle().\n\t\t\tForeground(m.FullColor).\n\t\t\tRender(strings.Repeat(string(m.Full), fw)))\n\t}\n\n\t// Empty fill.\n\tn := max(0, tw-fw)\n\tb.WriteString(lipgloss.NewStyle().\n\t\tForeground(m.EmptyColor).\n\t\tRender(strings.Repeat(string(m.Empty), n)))\n}\n\nfunc (m Model) percentageView(percent float64) string {\n\tif !m.ShowPercentage {\n\t\treturn \"\"\n\t}\n\tpercent = math.Max(0, math.Min(1, percent))\n\tpercentage := fmt.Sprintf(m.PercentFormat, percent*100) //nolint:mnd\n\tpercentage = m.PercentageStyle.Inline(true).Render(percentage)\n\treturn percentage\n}\n\n// IsAnimating returns false if the progress bar reached equilibrium and is no\n// longer animating.\nfunc (m *Model) IsAnimating() bool {\n\tdist := math.Abs(m.percentShown - m.targetPercent)\n\treturn !(dist < 0.001 && m.velocity < 0.01)\n}\n"
  },
  {
    "path": "progress/progress_test.go",
    "content": "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\"\n)\n\nfunc TestBlend(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\toptions []Option\n\t\twidth   int\n\t\tpercent float64\n\t}{\n\t\t{\n\t\t\tname: \"10w-red-to-green-50perc\",\n\t\t\toptions: []Option{\n\t\t\t\tWithColors(lipgloss.Color(\"#FF0000\"), lipgloss.Color(\"#00FF00\")),\n\t\t\t\tWithScaled(false),\n\t\t\t\tWithoutPercentage(),\n\t\t\t},\n\t\t\twidth:   10,\n\t\t\tpercent: 0.5,\n\t\t},\n\t\t{\n\t\t\tname: \"10w-red-to-green-50perc-full-block\",\n\t\t\toptions: []Option{\n\t\t\t\tWithColors(lipgloss.Color(\"#FF0000\"), lipgloss.Color(\"#00FF00\")),\n\t\t\t\tWithFillCharacters('█', DefaultEmptyCharBlock),\n\t\t\t\tWithoutPercentage(),\n\t\t\t},\n\t\t\twidth:   10,\n\t\t\tpercent: 0.5,\n\t\t},\n\t\t{\n\t\t\tname: \"30w-red-to-green-100perc\",\n\t\t\toptions: []Option{\n\t\t\t\tWithColors(lipgloss.Color(\"#FF0000\"), lipgloss.Color(\"#00FF00\")),\n\t\t\t\tWithScaled(false),\n\t\t\t\tWithoutPercentage(),\n\t\t\t},\n\t\t\twidth:   30,\n\t\t\tpercent: 1.0,\n\t\t},\n\t\t{\n\t\t\tname: \"10w-red-to-green-scaled-50perc\",\n\t\t\toptions: []Option{\n\t\t\t\tWithColors(lipgloss.Color(\"#FF0000\"), lipgloss.Color(\"#00FF00\")),\n\t\t\t\tWithScaled(true),\n\t\t\t\tWithoutPercentage(),\n\t\t\t},\n\t\t\twidth:   10,\n\t\t\tpercent: 0.5,\n\t\t},\n\t\t{\n\t\t\tname: \"30w-red-to-green-scaled-100perc\",\n\t\t\toptions: []Option{\n\t\t\t\tWithColors(lipgloss.Color(\"#FF0000\"), lipgloss.Color(\"#00FF00\")),\n\t\t\t\tWithScaled(true),\n\t\t\t\tWithoutPercentage(),\n\t\t\t},\n\t\t\twidth:   30,\n\t\t\tpercent: 1.0,\n\t\t},\n\t\t{\n\t\t\tname: \"30w-colorfunc-rgb-100perc\",\n\t\t\toptions: []Option{\n\t\t\t\tWithColorFunc(func(_, current float64) color.Color {\n\t\t\t\t\tif current <= 0.3 {\n\t\t\t\t\t\treturn lipgloss.Color(\"#FF0000\")\n\t\t\t\t\t}\n\t\t\t\t\tif current <= 0.7 {\n\t\t\t\t\t\treturn lipgloss.Color(\"#00FF00\")\n\t\t\t\t\t}\n\t\t\t\t\treturn lipgloss.Color(\"#0000FF\")\n\t\t\t\t}),\n\t\t\t\tWithoutPercentage(),\n\t\t\t},\n\t\t\twidth:   30,\n\t\t\tpercent: 1.0,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tp := New(tt.options...)\n\t\t\tp.SetWidth(tt.width)\n\t\t\tgolden.RequireEqual(t, []byte(p.ViewAs(tt.percent)))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "progress/testdata/TestBlend/10w-red-to-green-50perc-full-block.golden",
    "content": "\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░░░░░\u001b[m"
  },
  {
    "path": "progress/testdata/TestBlend/10w-red-to-green-50perc.golden",
    "content": "\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;48;2;219;145;0m▌\u001b[m\u001b[38;2;212;156;0;48;2;205;166;0m▌\u001b[m\u001b[38;2;96;96;96m░░░░░\u001b[m"
  },
  {
    "path": "progress/testdata/TestBlend/10w-red-to-green-scaled-50perc.golden",
    "content": "\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;0;48;2;144;220;0m▌\u001b[m\u001b[38;2;106;238;0;48;2;0;255;0m▌\u001b[m\u001b[38;2;96;96;96m░░░░░\u001b[m"
  },
  {
    "path": "progress/testdata/TestBlend/30w-colorfunc-rgb-100perc.golden",
    "content": "\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;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;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;0;255;0m▌\u001b[m\u001b[38;2;0;255;0;48;2;0;255;0m▌\u001b[m\u001b[38;2;0;255;0;48;2;0;255;0m▌\u001b[m\u001b[38;2;0;255;0;48;2;0;255;0m▌\u001b[m\u001b[38;2;0;255;0;48;2;0;255;0m▌\u001b[m\u001b[38;2;0;255;0;48;2;0;255;0m▌\u001b[m\u001b[38;2;0;255;0;48;2;0;255;0m▌\u001b[m\u001b[38;2;0;255;0;48;2;0;255;0m▌\u001b[m\u001b[38;2;0;255;0;48;2;0;255;0m▌\u001b[m\u001b[38;2;0;255;0;48;2;0;255;0m▌\u001b[m\u001b[38;2;0;255;0;48;2;0;255;0m▌\u001b[m\u001b[38;2;0;255;0;48;2;0;255;0m▌\u001b[m\u001b[38;2;0;255;0;48;2;0;0;255m▌\u001b[m\u001b[38;2;0;0;255;48;2;0;0;255m▌\u001b[m\u001b[38;2;0;0;255;48;2;0;0;255m▌\u001b[m\u001b[38;2;0;0;255;48;2;0;0;255m▌\u001b[m\u001b[38;2;0;0;255;48;2;0;0;255m▌\u001b[m\u001b[38;2;0;0;255;48;2;0;0;255m▌\u001b[m\u001b[38;2;0;0;255;48;2;0;0;255m▌\u001b[m\u001b[38;2;0;0;255;48;2;0;0;255m▌\u001b[m\u001b[38;2;0;0;255;48;2;0;0;255m▌\u001b[m\u001b[38;2;96;96;96m\u001b[m"
  },
  {
    "path": "progress/testdata/TestBlend/30w-red-to-green-100perc.golden",
    "content": "\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;2;245;81;0m▌\u001b[m\u001b[38;2;244;86;0;48;2;242;92;0m▌\u001b[m\u001b[38;2;241;97;0;48;2;239;102;0m▌\u001b[m\u001b[38;2;237;106;0;48;2;236;111;0m▌\u001b[m\u001b[38;2;234;115;0;48;2;232;119;0m▌\u001b[m\u001b[38;2;230;124;0;48;2;228;128;0m▌\u001b[m\u001b[38;2;227;131;0;48;2;225;135;0m▌\u001b[m\u001b[38;2;223;139;0;48;2;221;143;0m▌\u001b[m\u001b[38;2;218;146;0;48;2;216;150;0m▌\u001b[m\u001b[38;2;214;153;0;48;2;212;156;0m▌\u001b[m\u001b[38;2;209;160;0;48;2;207;163;0m▌\u001b[m\u001b[38;2;205;166;0;48;2;202;170;0m▌\u001b[m\u001b[38;2;200;173;0;48;2;197;176;0m▌\u001b[m\u001b[38;2;194;179;0;48;2;191;182;0m▌\u001b[m\u001b[38;2;188;185;0;48;2;185;188;0m▌\u001b[m\u001b[38;2;182;191;0;48;2;179;194;0m▌\u001b[m\u001b[38;2;176;197;0;48;2;172;200;0m▌\u001b[m\u001b[38;2;169;203;0;48;2;165;206;0m▌\u001b[m\u001b[38;2;161;209;0;48;2;157;212;0m▌\u001b[m\u001b[38;2;153;214;0;48;2;149;217;0m▌\u001b[m\u001b[38;2;144;220;0;48;2;139;223;0m▌\u001b[m\u001b[38;2;134;226;0;48;2;128;228;0m▌\u001b[m\u001b[38;2;123;231;0;48;2;116;234;0m▌\u001b[m\u001b[38;2;109;237;0;48;2;102;239;0m▌\u001b[m\u001b[38;2;94;242;0;48;2;84;245;0m▌\u001b[m\u001b[38;2;73;247;0;48;2;60;250;0m▌\u001b[m\u001b[38;2;41;253;0;48;2;0;255;0m▌\u001b[m\u001b[38;2;96;96;96m\u001b[m"
  },
  {
    "path": "progress/testdata/TestBlend/30w-red-to-green-scaled-100perc.golden",
    "content": "\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;2;245;81;0m▌\u001b[m\u001b[38;2;244;86;0;48;2;242;92;0m▌\u001b[m\u001b[38;2;241;97;0;48;2;239;102;0m▌\u001b[m\u001b[38;2;237;106;0;48;2;236;111;0m▌\u001b[m\u001b[38;2;234;115;0;48;2;232;119;0m▌\u001b[m\u001b[38;2;230;124;0;48;2;228;128;0m▌\u001b[m\u001b[38;2;227;131;0;48;2;225;135;0m▌\u001b[m\u001b[38;2;223;139;0;48;2;221;143;0m▌\u001b[m\u001b[38;2;218;146;0;48;2;216;150;0m▌\u001b[m\u001b[38;2;214;153;0;48;2;212;156;0m▌\u001b[m\u001b[38;2;209;160;0;48;2;207;163;0m▌\u001b[m\u001b[38;2;205;166;0;48;2;202;170;0m▌\u001b[m\u001b[38;2;200;173;0;48;2;197;176;0m▌\u001b[m\u001b[38;2;194;179;0;48;2;191;182;0m▌\u001b[m\u001b[38;2;188;185;0;48;2;185;188;0m▌\u001b[m\u001b[38;2;182;191;0;48;2;179;194;0m▌\u001b[m\u001b[38;2;176;197;0;48;2;172;200;0m▌\u001b[m\u001b[38;2;169;203;0;48;2;165;206;0m▌\u001b[m\u001b[38;2;161;209;0;48;2;157;212;0m▌\u001b[m\u001b[38;2;153;214;0;48;2;149;217;0m▌\u001b[m\u001b[38;2;144;220;0;48;2;139;223;0m▌\u001b[m\u001b[38;2;134;226;0;48;2;128;228;0m▌\u001b[m\u001b[38;2;123;231;0;48;2;116;234;0m▌\u001b[m\u001b[38;2;109;237;0;48;2;102;239;0m▌\u001b[m\u001b[38;2;94;242;0;48;2;84;245;0m▌\u001b[m\u001b[38;2;73;247;0;48;2;60;250;0m▌\u001b[m\u001b[38;2;41;253;0;48;2;0;255;0m▌\u001b[m\u001b[38;2;96;96;96m\u001b[m"
  },
  {
    "path": "spinner/spinner.go",
    "content": "// Package spinner provides a spinner component for Bubble Tea applications.\npackage spinner\n\nimport (\n\t\"sync/atomic\"\n\t\"time\"\n\n\ttea \"charm.land/bubbletea/v2\"\n\t\"charm.land/lipgloss/v2\"\n)\n\n// Internal ID management. Used during animating to ensure that frame messages\n// are received only by spinner components that sent them.\nvar lastID int64\n\nfunc nextID() int {\n\treturn int(atomic.AddInt64(&lastID, 1))\n}\n\n// Spinner is a set of frames used in animating the spinner.\ntype Spinner struct {\n\tFrames []string\n\tFPS    time.Duration\n}\n\n// Some spinners to choose from. You could also make your own.\nvar (\n\tLine = Spinner{\n\t\tFrames: []string{\"|\", \"/\", \"-\", \"\\\\\"},\n\t\tFPS:    time.Second / 10, //nolint:mnd\n\t}\n\tDot = Spinner{\n\t\tFrames: []string{\"⣾ \", \"⣽ \", \"⣻ \", \"⢿ \", \"⡿ \", \"⣟ \", \"⣯ \", \"⣷ \"},\n\t\tFPS:    time.Second / 10, //nolint:mnd\n\t}\n\tMiniDot = Spinner{\n\t\tFrames: []string{\"⠋\", \"⠙\", \"⠹\", \"⠸\", \"⠼\", \"⠴\", \"⠦\", \"⠧\", \"⠇\", \"⠏\"},\n\t\tFPS:    time.Second / 12, //nolint:mnd\n\t}\n\tJump = Spinner{\n\t\tFrames: []string{\"⢄\", \"⢂\", \"⢁\", \"⡁\", \"⡈\", \"⡐\", \"⡠\"},\n\t\tFPS:    time.Second / 10, //nolint:mnd\n\t}\n\tPulse = Spinner{\n\t\tFrames: []string{\"█\", \"▓\", \"▒\", \"░\"},\n\t\tFPS:    time.Second / 8, //nolint:mnd\n\t}\n\tPoints = Spinner{\n\t\tFrames: []string{\"∙∙∙\", \"●∙∙\", \"∙●∙\", \"∙∙●\"},\n\t\tFPS:    time.Second / 7, //nolint:mnd\n\t}\n\tGlobe = Spinner{\n\t\tFrames: []string{\"🌍\", \"🌎\", \"🌏\"},\n\t\tFPS:    time.Second / 4, //nolint:mnd\n\t}\n\tMoon = Spinner{\n\t\tFrames: []string{\"🌑\", \"🌒\", \"🌓\", \"🌔\", \"🌕\", \"🌖\", \"🌗\", \"🌘\"},\n\t\tFPS:    time.Second / 8, //nolint:mnd\n\t}\n\tMonkey = Spinner{\n\t\tFrames: []string{\"🙈\", \"🙉\", \"🙊\"},\n\t\tFPS:    time.Second / 3, //nolint:mnd\n\t}\n\tMeter = Spinner{\n\t\tFrames: []string{\n\t\t\t\"▱▱▱\",\n\t\t\t\"▰▱▱\",\n\t\t\t\"▰▰▱\",\n\t\t\t\"▰▰▰\",\n\t\t\t\"▰▰▱\",\n\t\t\t\"▰▱▱\",\n\t\t\t\"▱▱▱\",\n\t\t},\n\t\tFPS: time.Second / 7, //nolint:mnd\n\t}\n\tHamburger = Spinner{\n\t\tFrames: []string{\"☱\", \"☲\", \"☴\", \"☲\"},\n\t\tFPS:    time.Second / 3, //nolint:mnd\n\t}\n\tEllipsis = Spinner{\n\t\tFrames: []string{\"\", \".\", \"..\", \"...\"},\n\t\tFPS:    time.Second / 3, //nolint:mnd\n\t}\n)\n\n// Model contains the state for the spinner. Use New to create new models\n// rather than using Model as a struct literal.\ntype Model struct {\n\t// Spinner settings to use. See type Spinner.\n\tSpinner Spinner\n\n\t// Style sets the styling for the spinner. Most of the time you'll just\n\t// want foreground and background coloring, and potentially some padding.\n\t//\n\t// For an introduction to styling with Lip Gloss see:\n\t// https://github.com/charmbracelet/lipgloss\n\tStyle lipgloss.Style\n\n\tframe int\n\tid    int\n\ttag   int\n}\n\n// ID returns the spinner's unique ID.\nfunc (m Model) ID() int {\n\treturn m.id\n}\n\n// New returns a model with default values.\nfunc New(opts ...Option) Model {\n\tm := Model{\n\t\tSpinner: Line,\n\t\tid:      nextID(),\n\t}\n\n\tfor _, opt := range opts {\n\t\topt(&m)\n\t}\n\n\treturn m\n}\n\n// TickMsg indicates that the timer has ticked and we should render a frame.\ntype TickMsg struct {\n\tTime time.Time\n\ttag  int\n\tID   int\n}\n\n// Update is the Tea update function.\nfunc (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {\n\tswitch msg := msg.(type) {\n\tcase TickMsg:\n\t\t// If an ID is set, and the ID doesn't belong to this spinner, reject\n\t\t// the message.\n\t\tif msg.ID > 0 && msg.ID != m.id {\n\t\t\treturn m, nil\n\t\t}\n\n\t\t// If a tag is set, and it's not the one we expect, reject the message.\n\t\t// This prevents the spinner from receiving too many messages and\n\t\t// thus spinning too fast.\n\t\tif msg.tag > 0 && msg.tag != m.tag {\n\t\t\treturn m, nil\n\t\t}\n\n\t\tm.frame++\n\t\tif m.frame >= len(m.Spinner.Frames) {\n\t\t\tm.frame = 0\n\t\t}\n\n\t\tm.tag++\n\t\treturn m, m.tick(m.id, m.tag)\n\tdefault:\n\t\treturn m, nil\n\t}\n}\n\n// View renders the model's view.\nfunc (m Model) View() string {\n\tif m.frame >= len(m.Spinner.Frames) {\n\t\treturn \"(error)\"\n\t}\n\n\treturn m.Style.Render(m.Spinner.Frames[m.frame])\n}\n\n// Tick is the command used to advance the spinner one frame. Use this command\n// to effectively start the spinner.\nfunc (m Model) Tick() tea.Msg {\n\treturn TickMsg{\n\t\t// The time at which the tick occurred.\n\t\tTime: time.Now(),\n\n\t\t// The ID of the spinner that this message belongs to. This can be\n\t\t// helpful when routing messages, however bear in mind that spinners\n\t\t// will ignore messages that don't contain ID by default.\n\t\tID: m.id,\n\n\t\ttag: m.tag,\n\t}\n}\n\nfunc (m Model) tick(id, tag int) tea.Cmd {\n\treturn tea.Tick(m.Spinner.FPS, func(t time.Time) tea.Msg {\n\t\treturn TickMsg{\n\t\t\tTime: t,\n\t\t\tID:   id,\n\t\t\ttag:  tag,\n\t\t}\n\t})\n}\n\n// Option is used to set options in New. For example:\n//\n//\tspinner := New(WithSpinner(Dot))\ntype Option func(*Model)\n\n// WithSpinner is an option to set the spinner. Pass this to [Spinner.New].\nfunc WithSpinner(spinner Spinner) Option {\n\treturn func(m *Model) {\n\t\tm.Spinner = spinner\n\t}\n}\n\n// WithStyle is an option to set the spinner style. Pass this to [Spinner.New].\nfunc WithStyle(style lipgloss.Style) Option {\n\treturn func(m *Model) {\n\t\tm.Style = style\n\t}\n}\n"
  },
  {
    "path": "spinner/spinner_test.go",
    "content": "package spinner_test\n\nimport (\n\t\"testing\"\n\n\t\"charm.land/bubbles/v2/spinner\"\n)\n\nfunc TestSpinnerNew(t *testing.T) {\n\tassertEqualSpinner := func(t *testing.T, exp, got spinner.Spinner) {\n\t\tt.Helper()\n\n\t\tif exp.FPS != got.FPS {\n\t\t\tt.Errorf(\"expecting %d FPS, got %d\", exp.FPS, got.FPS)\n\t\t}\n\n\t\tif e, g := len(exp.Frames), len(got.Frames); e != g {\n\t\t\tt.Fatalf(\"expecting %d frames, got %d\", e, g)\n\t\t}\n\n\t\tfor i, e := range exp.Frames {\n\t\t\tif g := got.Frames[i]; e != g {\n\t\t\t\tt.Errorf(\"expecting frame index %d with value %q, got %q\", i, e, g)\n\t\t\t}\n\t\t}\n\t}\n\tt.Run(\"default\", func(t *testing.T) {\n\t\ts := spinner.New()\n\n\t\tassertEqualSpinner(t, spinner.Line, s.Spinner)\n\t})\n\n\tt.Run(\"WithSpinner\", func(t *testing.T) {\n\t\tcustomSpinner := spinner.Spinner{\n\t\t\tFrames: []string{\"a\", \"b\", \"c\", \"d\"},\n\t\t\tFPS:    16,\n\t\t}\n\n\t\ts := spinner.New(spinner.WithSpinner(customSpinner))\n\n\t\tassertEqualSpinner(t, customSpinner, s.Spinner)\n\t})\n\n\ttests := map[string]spinner.Spinner{\n\t\t\"Line\":    spinner.Line,\n\t\t\"Dot\":     spinner.Dot,\n\t\t\"MiniDot\": spinner.MiniDot,\n\t\t\"Jump\":    spinner.Jump,\n\t\t\"Pulse\":   spinner.Pulse,\n\t\t\"Points\":  spinner.Points,\n\t\t\"Globe\":   spinner.Globe,\n\t\t\"Moon\":    spinner.Moon,\n\t\t\"Monkey\":  spinner.Monkey,\n\t}\n\n\tfor name, s := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tassertEqualSpinner(t, spinner.New(spinner.WithSpinner(s)).Spinner, s)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "stopwatch/stopwatch.go",
    "content": "// Package stopwatch provides a simple stopwatch component.\npackage stopwatch\n\nimport (\n\t\"sync/atomic\"\n\t\"time\"\n\n\ttea \"charm.land/bubbletea/v2\"\n)\n\nvar lastID int64\n\nfunc nextID() int {\n\treturn int(atomic.AddInt64(&lastID, 1))\n}\n\n// Option is a configuration option in [New]. For example:\n//\n//\ttimer := New(time.Second*10, WithInterval(5*time.Second))\ntype Option func(*Model)\n\n// WithInterval is an option for setting the interval between ticks. Pass as\n// an argument to [New].\nfunc WithInterval(interval time.Duration) Option {\n\treturn func(m *Model) {\n\t\tm.Interval = interval\n\t}\n}\n\n// TickMsg is a message that is sent on every timer tick.\ntype TickMsg struct {\n\t// ID is the identifier of the stopwatch that sends the message. This makes\n\t// it possible to determine which stopwatch a tick belongs to when there\n\t// are multiple stopwatches running.\n\t//\n\t// Note, however, that a stopwatch will reject ticks from other\n\t// stopwatches, so it's safe to flow all TickMsgs through all stopwatches\n\t// and have them still behave appropriately.\n\tID  int\n\ttag int\n}\n\n// StartStopMsg is sent when the stopwatch should start or stop.\ntype StartStopMsg struct {\n\tID      int\n\trunning bool\n}\n\n// ResetMsg is sent when the stopwatch should reset.\ntype ResetMsg struct {\n\tID int\n}\n\n// Model for the stopwatch component.\ntype Model struct {\n\td       time.Duration\n\tid      int\n\ttag     int\n\trunning bool\n\n\t// How long to wait before every tick. Defaults to 1 second.\n\tInterval time.Duration\n}\n\n// New creates a new stopwatch with 1s interval.\nfunc New(opts ...Option) Model {\n\tm := Model{\n\t\tid: nextID(),\n\t}\n\n\tfor _, opt := range opts {\n\t\topt(&m)\n\t}\n\treturn m\n}\n\n// ID returns the unique ID of the model.\nfunc (m Model) ID() int {\n\treturn m.id\n}\n\n// Init starts the stopwatch.\nfunc (m Model) Init() tea.Cmd {\n\treturn m.Start()\n}\n\n// Start starts the stopwatch.\nfunc (m Model) Start() tea.Cmd {\n\treturn tea.Sequence(func() tea.Msg {\n\t\treturn StartStopMsg{ID: m.id, running: true}\n\t}, tick(m.id, m.tag, m.Interval))\n}\n\n// Stop stops the stopwatch.\nfunc (m Model) Stop() tea.Cmd {\n\treturn func() tea.Msg {\n\t\treturn StartStopMsg{ID: m.id, running: false}\n\t}\n}\n\n// Toggle stops the stopwatch if it is running and starts it if it is stopped.\nfunc (m Model) Toggle() tea.Cmd {\n\tif m.Running() {\n\t\treturn m.Stop()\n\t}\n\treturn m.Start()\n}\n\n// Reset resets the stopwatch to 0.\nfunc (m Model) Reset() tea.Cmd {\n\treturn func() tea.Msg {\n\t\treturn ResetMsg{ID: m.id}\n\t}\n}\n\n// Running returns true if the stopwatch is running or false if it is stopped.\nfunc (m Model) Running() bool {\n\treturn m.running\n}\n\n// Update handles the timer tick.\nfunc (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {\n\tswitch msg := msg.(type) {\n\tcase StartStopMsg:\n\t\tif msg.ID != m.id {\n\t\t\treturn m, nil\n\t\t}\n\t\tm.running = msg.running\n\tcase ResetMsg:\n\t\tif msg.ID != m.id {\n\t\t\treturn m, nil\n\t\t}\n\t\tm.d = 0\n\tcase TickMsg:\n\t\tif !m.running || msg.ID != m.id {\n\t\t\tbreak\n\t\t}\n\n\t\t// If a tag is set, and it's not the one we expect, reject the message.\n\t\t// This prevents the stopwatch from receiving too many messages and\n\t\t// thus ticking too fast.\n\t\tif msg.tag > 0 && msg.tag != m.tag {\n\t\t\treturn m, nil\n\t\t}\n\n\t\tm.d += m.Interval\n\t\tm.tag++\n\t\treturn m, tick(m.id, m.tag, m.Interval)\n\t}\n\n\treturn m, nil\n}\n\n// Elapsed returns the time elapsed.\nfunc (m Model) Elapsed() time.Duration {\n\treturn m.d\n}\n\n// View of the timer component.\nfunc (m Model) View() string {\n\treturn m.d.String()\n}\n\nfunc tick(id int, tag int, d time.Duration) tea.Cmd {\n\treturn tea.Tick(d, func(_ time.Time) tea.Msg {\n\t\treturn TickMsg{ID: id, tag: tag}\n\t})\n}\n"
  },
  {
    "path": "table/table.go",
    "content": "// Package table provides a simple table component for Bubble Tea applications.\npackage table\n\nimport (\n\t\"strings\"\n\n\t\"charm.land/bubbles/v2/help\"\n\t\"charm.land/bubbles/v2/key\"\n\t\"charm.land/bubbles/v2/viewport\"\n\ttea \"charm.land/bubbletea/v2\"\n\t\"charm.land/lipgloss/v2\"\n\t\"github.com/charmbracelet/x/ansi\"\n)\n\n// Model defines a state for the table widget.\ntype Model struct {\n\tKeyMap KeyMap\n\tHelp   help.Model\n\n\tcols   []Column\n\trows   []Row\n\tcursor int\n\tfocus  bool\n\tstyles Styles\n\n\tviewport viewport.Model\n\tstart    int\n\tend      int\n}\n\n// Row represents one line in the table.\ntype Row []string\n\n// Column defines the table structure.\ntype Column struct {\n\tTitle string\n\tWidth int\n}\n\n// KeyMap defines keybindings. It satisfies to the help.KeyMap interface, which\n// is used to render the help menu.\ntype KeyMap struct {\n\tLineUp       key.Binding\n\tLineDown     key.Binding\n\tPageUp       key.Binding\n\tPageDown     key.Binding\n\tHalfPageUp   key.Binding\n\tHalfPageDown key.Binding\n\tGotoTop      key.Binding\n\tGotoBottom   key.Binding\n}\n\n// ShortHelp implements the KeyMap interface.\nfunc (km KeyMap) ShortHelp() []key.Binding {\n\treturn []key.Binding{km.LineUp, km.LineDown}\n}\n\n// FullHelp implements the KeyMap interface.\nfunc (km KeyMap) FullHelp() [][]key.Binding {\n\treturn [][]key.Binding{\n\t\t{km.LineUp, km.LineDown, km.GotoTop, km.GotoBottom},\n\t\t{km.PageUp, km.PageDown, km.HalfPageUp, km.HalfPageDown},\n\t}\n}\n\n// DefaultKeyMap returns a default set of keybindings.\nfunc DefaultKeyMap() KeyMap {\n\treturn KeyMap{\n\t\tLineUp: key.NewBinding(\n\t\t\tkey.WithKeys(\"up\", \"k\"),\n\t\t\tkey.WithHelp(\"↑/k\", \"up\"),\n\t\t),\n\t\tLineDown: key.NewBinding(\n\t\t\tkey.WithKeys(\"down\", \"j\"),\n\t\t\tkey.WithHelp(\"↓/j\", \"down\"),\n\t\t),\n\t\tPageUp: key.NewBinding(\n\t\t\tkey.WithKeys(\"b\", \"pgup\"),\n\t\t\tkey.WithHelp(\"b/pgup\", \"page up\"),\n\t\t),\n\t\tPageDown: key.NewBinding(\n\t\t\tkey.WithKeys(\"f\", \"pgdown\", \"space\"),\n\t\t\tkey.WithHelp(\"f/pgdn\", \"page down\"),\n\t\t),\n\t\tHalfPageUp: key.NewBinding(\n\t\t\tkey.WithKeys(\"u\", \"ctrl+u\"),\n\t\t\tkey.WithHelp(\"u\", \"½ page up\"),\n\t\t),\n\t\tHalfPageDown: key.NewBinding(\n\t\t\tkey.WithKeys(\"d\", \"ctrl+d\"),\n\t\t\tkey.WithHelp(\"d\", \"½ page down\"),\n\t\t),\n\t\tGotoTop: key.NewBinding(\n\t\t\tkey.WithKeys(\"home\", \"g\"),\n\t\t\tkey.WithHelp(\"g/home\", \"go to start\"),\n\t\t),\n\t\tGotoBottom: key.NewBinding(\n\t\t\tkey.WithKeys(\"end\", \"G\"),\n\t\t\tkey.WithHelp(\"G/end\", \"go to end\"),\n\t\t),\n\t}\n}\n\n// Styles contains style definitions for this list component. By default, these\n// values are generated by DefaultStyles.\ntype Styles struct {\n\tHeader   lipgloss.Style\n\tCell     lipgloss.Style\n\tSelected lipgloss.Style\n}\n\n// DefaultStyles returns a set of default style definitions for this table.\nfunc DefaultStyles() Styles {\n\treturn Styles{\n\t\tSelected: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color(\"212\")),\n\t\tHeader:   lipgloss.NewStyle().Bold(true).Padding(0, 1),\n\t\tCell:     lipgloss.NewStyle().Padding(0, 1),\n\t}\n}\n\n// SetStyles sets the table styles.\nfunc (m *Model) SetStyles(s Styles) {\n\tm.styles = s\n\tm.UpdateViewport()\n}\n\n// Option is used to set options in New. For example:\n//\n//\ttable := New(WithColumns([]Column{{Title: \"ID\", Width: 10}}))\ntype Option func(*Model)\n\n// New creates a new model for the table widget.\nfunc New(opts ...Option) Model {\n\tm := Model{\n\t\tcursor:   0,\n\t\tviewport: viewport.New(viewport.WithHeight(20)), //nolint:mnd\n\n\t\tKeyMap: DefaultKeyMap(),\n\t\tHelp:   help.New(),\n\t\tstyles: DefaultStyles(),\n\t}\n\n\tfor _, opt := range opts {\n\t\topt(&m)\n\t}\n\n\tm.UpdateViewport()\n\n\treturn m\n}\n\n// WithColumns sets the table columns (headers).\nfunc WithColumns(cols []Column) Option {\n\treturn func(m *Model) {\n\t\tm.cols = cols\n\t}\n}\n\n// WithRows sets the table rows (data).\nfunc WithRows(rows []Row) Option {\n\treturn func(m *Model) {\n\t\tm.rows = rows\n\t}\n}\n\n// WithHeight sets the height of the table.\nfunc WithHeight(h int) Option {\n\treturn func(m *Model) {\n\t\tm.viewport.SetHeight(h - lipgloss.Height(m.headersView()))\n\t}\n}\n\n// WithWidth sets the width of the table.\nfunc WithWidth(w int) Option {\n\treturn func(m *Model) {\n\t\tm.viewport.SetWidth(w)\n\t}\n}\n\n// WithFocused sets the focus state of the table.\nfunc WithFocused(f bool) Option {\n\treturn func(m *Model) {\n\t\tm.focus = f\n\t}\n}\n\n// WithStyles sets the table styles.\nfunc WithStyles(s Styles) Option {\n\treturn func(m *Model) {\n\t\tm.styles = s\n\t}\n}\n\n// WithKeyMap sets the key map.\nfunc WithKeyMap(km KeyMap) Option {\n\treturn func(m *Model) {\n\t\tm.KeyMap = km\n\t}\n}\n\n// Update is the Bubble Tea update loop.\nfunc (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {\n\tif !m.focus {\n\t\treturn m, nil\n\t}\n\n\tswitch msg := msg.(type) {\n\tcase tea.KeyPressMsg:\n\t\tswitch {\n\t\tcase key.Matches(msg, m.KeyMap.LineUp):\n\t\t\tm.MoveUp(1)\n\t\tcase key.Matches(msg, m.KeyMap.LineDown):\n\t\t\tm.MoveDown(1)\n\t\tcase key.Matches(msg, m.KeyMap.PageUp):\n\t\t\tm.MoveUp(m.viewport.Height())\n\t\tcase key.Matches(msg, m.KeyMap.PageDown):\n\t\t\tm.MoveDown(m.viewport.Height())\n\t\tcase key.Matches(msg, m.KeyMap.HalfPageUp):\n\t\t\tm.MoveUp(m.viewport.Height() / 2) //nolint:mnd\n\t\tcase key.Matches(msg, m.KeyMap.HalfPageDown):\n\t\t\tm.MoveDown(m.viewport.Height() / 2) //nolint:mnd\n\t\tcase key.Matches(msg, m.KeyMap.GotoTop):\n\t\t\tm.GotoTop()\n\t\tcase key.Matches(msg, m.KeyMap.GotoBottom):\n\t\t\tm.GotoBottom()\n\t\t}\n\t}\n\n\treturn m, nil\n}\n\n// Focused returns the focus state of the table.\nfunc (m Model) Focused() bool {\n\treturn m.focus\n}\n\n// Focus focuses the table, allowing the user to move around the rows and\n// interact.\nfunc (m *Model) Focus() {\n\tm.focus = true\n\tm.UpdateViewport()\n}\n\n// Blur blurs the table, preventing selection or movement.\nfunc (m *Model) Blur() {\n\tm.focus = false\n\tm.UpdateViewport()\n}\n\n// View renders the component.\nfunc (m Model) View() string {\n\treturn m.headersView() + \"\\n\" + m.viewport.View()\n}\n\n// HelpView is a helper method for rendering the help menu from the keymap.\n// Note that this view is not rendered by default and you must call it\n// manually in your application, where applicable.\nfunc (m Model) HelpView() string {\n\treturn m.Help.View(m.KeyMap)\n}\n\n// UpdateViewport updates the list content based on the previously defined\n// columns and rows.\nfunc (m *Model) UpdateViewport() {\n\trenderedRows := make([]string, 0, len(m.rows))\n\n\t// Render only rows from: m.cursor-m.viewport.Height to: m.cursor+m.viewport.Height\n\t// Constant runtime, independent of number of rows in a table.\n\t// Limits the number of renderedRows to a maximum of 2*m.viewport.Height\n\tif m.cursor >= 0 {\n\t\tm.start = clamp(m.cursor-m.viewport.Height(), 0, m.cursor)\n\t} else {\n\t\tm.start = 0\n\t}\n\tm.end = clamp(m.cursor+m.viewport.Height(), m.cursor, len(m.rows))\n\tfor i := m.start; i < m.end; i++ {\n\t\trenderedRows = append(renderedRows, m.renderRow(i))\n\t}\n\n\tm.viewport.SetContent(\n\t\tlipgloss.JoinVertical(lipgloss.Left, renderedRows...),\n\t)\n}\n\n// SelectedRow returns the selected row.\n// You can cast it to your own implementation.\nfunc (m Model) SelectedRow() Row {\n\tif m.cursor < 0 || m.cursor >= len(m.rows) {\n\t\treturn nil\n\t}\n\n\treturn m.rows[m.cursor]\n}\n\n// Rows returns the current rows.\nfunc (m Model) Rows() []Row {\n\treturn m.rows\n}\n\n// Columns returns the current columns.\nfunc (m Model) Columns() []Column {\n\treturn m.cols\n}\n\n// SetRows sets a new rows state.\nfunc (m *Model) SetRows(r []Row) {\n\tm.rows = r\n\n\tif m.cursor > len(m.rows)-1 {\n\t\tm.cursor = len(m.rows) - 1\n\t}\n\n\tm.UpdateViewport()\n}\n\n// SetColumns sets a new columns state.\nfunc (m *Model) SetColumns(c []Column) {\n\tm.cols = c\n\tm.UpdateViewport()\n}\n\n// SetWidth sets the width of the viewport of the table.\nfunc (m *Model) SetWidth(w int) {\n\tm.viewport.SetWidth(w)\n\tm.UpdateViewport()\n}\n\n// SetHeight sets the height of the viewport of the table.\nfunc (m *Model) SetHeight(h int) {\n\tm.viewport.SetHeight(h - lipgloss.Height(m.headersView()))\n\tm.UpdateViewport()\n}\n\n// Height returns the viewport height of the table.\nfunc (m Model) Height() int {\n\treturn m.viewport.Height()\n}\n\n// Width returns the viewport width of the table.\nfunc (m Model) Width() int {\n\treturn m.viewport.Width()\n}\n\n// Cursor returns the index of the selected row.\nfunc (m Model) Cursor() int {\n\treturn m.cursor\n}\n\n// SetCursor sets the cursor position in the table.\nfunc (m *Model) SetCursor(n int) {\n\tm.cursor = clamp(n, 0, len(m.rows)-1)\n\tm.UpdateViewport()\n}\n\n// MoveUp moves the selection up by any number of rows.\n// It can not go above the first row.\nfunc (m *Model) MoveUp(n int) {\n\tm.cursor = clamp(m.cursor-n, 0, len(m.rows)-1)\n\n\toffset := m.viewport.YOffset()\n\tswitch {\n\tcase m.start == 0:\n\t\toffset = clamp(offset, 0, m.cursor)\n\tcase m.start < m.viewport.Height():\n\t\toffset = clamp(clamp(offset+n, 0, m.cursor), 0, m.viewport.Height())\n\tcase offset >= 1:\n\t\toffset = clamp(offset+n, 1, m.viewport.Height())\n\t}\n\tm.viewport.SetYOffset(offset)\n\tm.UpdateViewport()\n}\n\n// MoveDown moves the selection down by any number of rows.\n// It can not go below the last row.\nfunc (m *Model) MoveDown(n int) {\n\tm.cursor = clamp(m.cursor+n, 0, len(m.rows)-1)\n\tm.UpdateViewport()\n\n\toffset := m.viewport.YOffset()\n\tswitch {\n\tcase m.end == len(m.rows) && offset > 0:\n\t\toffset = clamp(offset-n, 1, m.viewport.Height())\n\tcase m.cursor > (m.end-m.start)/2 && offset > 0:\n\t\toffset = clamp(offset-n, 1, m.cursor)\n\tcase offset > 1:\n\tcase m.cursor > offset+m.viewport.Height()-1:\n\t\toffset = clamp(offset+1, 0, 1)\n\t}\n\tm.viewport.SetYOffset(offset)\n}\n\n// GotoTop moves the selection to the first row.\nfunc (m *Model) GotoTop() {\n\tm.MoveUp(m.cursor)\n}\n\n// GotoBottom moves the selection to the last row.\nfunc (m *Model) GotoBottom() {\n\tm.MoveDown(len(m.rows))\n}\n\n// FromValues create the table rows from a simple string. It uses `\\n` by\n// default for getting all the rows and the given separator for the fields on\n// each row.\nfunc (m *Model) FromValues(value, separator string) {\n\trows := []Row{} //nolint:prealloc\n\tfor _, line := range strings.Split(value, \"\\n\") {\n\t\tr := Row{}\n\t\tfor _, field := range strings.Split(line, separator) {\n\t\t\tr = append(r, field)\n\t\t}\n\t\trows = append(rows, r)\n\t}\n\n\tm.SetRows(rows)\n}\n\nfunc (m Model) headersView() string {\n\ts := make([]string, 0, len(m.cols))\n\tfor _, col := range m.cols {\n\t\tif col.Width <= 0 {\n\t\t\tcontinue\n\t\t}\n\t\tstyle := lipgloss.NewStyle().Width(col.Width).MaxWidth(col.Width).Inline(true)\n\t\trenderedCell := style.Render(ansi.Truncate(col.Title, col.Width, \"…\"))\n\t\ts = append(s, m.styles.Header.Render(renderedCell))\n\t}\n\treturn lipgloss.JoinHorizontal(lipgloss.Top, s...)\n}\n\nfunc (m *Model) renderRow(r int) string {\n\ts := make([]string, 0, len(m.cols))\n\tfor i, value := range m.rows[r] {\n\t\tif m.cols[i].Width <= 0 {\n\t\t\tcontinue\n\t\t}\n\t\tstyle := lipgloss.NewStyle().Width(m.cols[i].Width).MaxWidth(m.cols[i].Width).Inline(true)\n\t\trenderedCell := m.styles.Cell.Render(style.Render(ansi.Truncate(value, m.cols[i].Width, \"…\")))\n\t\ts = append(s, renderedCell)\n\t}\n\n\trow := lipgloss.JoinHorizontal(lipgloss.Top, s...)\n\n\tif r == m.cursor {\n\t\treturn m.styles.Selected.Render(row)\n\t}\n\n\treturn row\n}\n\nfunc clamp(v, low, high int) int {\n\treturn min(max(v, low), high)\n}\n"
  },
  {
    "path": "table/table_test.go",
    "content": "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\"\n\t\"charm.land/lipgloss/v2\"\n\t\"github.com/charmbracelet/x/ansi\"\n\t\"github.com/charmbracelet/x/exp/golden\"\n)\n\nvar testCols = []Column{\n\t{Title: \"col1\", Width: 10},\n\t{Title: \"col2\", Width: 10},\n\t{Title: \"col3\", Width: 10},\n}\n\nfunc TestNew(t *testing.T) {\n\ttests := map[string]struct {\n\t\topts []Option\n\t\twant Model\n\t}{\n\t\t\"Default\": {\n\t\t\twant: Model{\n\t\t\t\t// Default fields\n\t\t\t\tcursor: 0,\n\t\t\t\tviewport: viewport.New(\n\t\t\t\t\tviewport.WithWidth(0),\n\t\t\t\t\tviewport.WithHeight(20),\n\t\t\t\t),\n\t\t\t\tKeyMap: DefaultKeyMap(),\n\t\t\t\tHelp:   help.New(),\n\t\t\t\tstyles: DefaultStyles(),\n\t\t\t},\n\t\t},\n\t\t\"WithColumns\": {\n\t\t\topts: []Option{\n\t\t\t\tWithColumns([]Column{\n\t\t\t\t\t{Title: \"Foo\", Width: 1},\n\t\t\t\t\t{Title: \"Bar\", Width: 2},\n\t\t\t\t}),\n\t\t\t},\n\t\t\twant: Model{\n\t\t\t\t// Default fields\n\t\t\t\tcursor: 0,\n\t\t\t\tviewport: viewport.New(\n\t\t\t\t\tviewport.WithWidth(0),\n\t\t\t\t\tviewport.WithHeight(20),\n\t\t\t\t),\n\t\t\t\tKeyMap: DefaultKeyMap(),\n\t\t\t\tHelp:   help.New(),\n\t\t\t\tstyles: DefaultStyles(),\n\n\t\t\t\t// Modified fields\n\t\t\t\tcols: []Column{\n\t\t\t\t\t{Title: \"Foo\", Width: 1},\n\t\t\t\t\t{Title: \"Bar\", Width: 2},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t\"WithColumns; WithRows\": {\n\t\t\topts: []Option{\n\t\t\t\tWithColumns([]Column{\n\t\t\t\t\t{Title: \"Foo\", Width: 1},\n\t\t\t\t\t{Title: \"Bar\", Width: 2},\n\t\t\t\t}),\n\t\t\t\tWithRows([]Row{\n\t\t\t\t\t{\"1\", \"Foo\"},\n\t\t\t\t\t{\"2\", \"Bar\"},\n\t\t\t\t}),\n\t\t\t},\n\t\t\twant: Model{\n\t\t\t\t// Default fields\n\t\t\t\tcursor: 0,\n\t\t\t\tviewport: viewport.New(\n\t\t\t\t\tviewport.WithWidth(0),\n\t\t\t\t\tviewport.WithHeight(20),\n\t\t\t\t),\n\t\t\t\tKeyMap: DefaultKeyMap(),\n\t\t\t\tHelp:   help.New(),\n\t\t\t\tstyles: DefaultStyles(),\n\n\t\t\t\t// Modified fields\n\t\t\t\tcols: []Column{\n\t\t\t\t\t{Title: \"Foo\", Width: 1},\n\t\t\t\t\t{Title: \"Bar\", Width: 2},\n\t\t\t\t},\n\t\t\t\trows: []Row{\n\t\t\t\t\t{\"1\", \"Foo\"},\n\t\t\t\t\t{\"2\", \"Bar\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t\"WithHeight\": {\n\t\t\topts: []Option{\n\t\t\t\tWithHeight(10),\n\t\t\t},\n\t\t\twant: Model{\n\t\t\t\t// Default fields\n\t\t\t\tcursor: 0,\n\t\t\t\tKeyMap: DefaultKeyMap(),\n\t\t\t\tHelp:   help.New(),\n\t\t\t\tstyles: DefaultStyles(),\n\n\t\t\t\t// Modified fields\n\t\t\t\t// Viewport height is 1 less than the provided height when no header is present since lipgloss.Height adds 1\n\t\t\t\tviewport: viewport.New(\n\t\t\t\t\tviewport.WithWidth(0),\n\t\t\t\t\tviewport.WithHeight(9),\n\t\t\t\t),\n\t\t\t},\n\t\t},\n\t\t\"WithWidth\": {\n\t\t\topts: []Option{\n\t\t\t\tWithWidth(10),\n\t\t\t},\n\t\t\twant: Model{\n\t\t\t\t// Default fields\n\t\t\t\tcursor: 0,\n\t\t\t\tKeyMap: DefaultKeyMap(),\n\t\t\t\tHelp:   help.New(),\n\t\t\t\tstyles: DefaultStyles(),\n\n\t\t\t\t// Modified fields\n\t\t\t\t// Viewport height is 1 less than the provided height when no header is present since lipgloss.Height adds 1\n\t\t\t\tviewport: viewport.New(\n\t\t\t\t\tviewport.WithWidth(10),\n\t\t\t\t\tviewport.WithHeight(20),\n\t\t\t\t),\n\t\t\t},\n\t\t},\n\t\t\"WithFocused\": {\n\t\t\topts: []Option{\n\t\t\t\tWithFocused(true),\n\t\t\t},\n\t\t\twant: Model{\n\t\t\t\t// Default fields\n\t\t\t\tcursor: 0,\n\t\t\t\tviewport: viewport.New(\n\t\t\t\t\tviewport.WithWidth(0),\n\t\t\t\t\tviewport.WithHeight(20),\n\t\t\t\t),\n\t\t\t\tKeyMap: DefaultKeyMap(),\n\t\t\t\tHelp:   help.New(),\n\t\t\t\tstyles: DefaultStyles(),\n\n\t\t\t\t// Modified fields\n\t\t\t\tfocus: true,\n\t\t\t},\n\t\t},\n\t\t\"WithStyles\": {\n\t\t\topts: []Option{\n\t\t\t\tWithStyles(Styles{}),\n\t\t\t},\n\t\t\twant: Model{\n\t\t\t\t// Default fields\n\t\t\t\tcursor: 0,\n\t\t\t\tviewport: viewport.New(\n\t\t\t\t\tviewport.WithWidth(0),\n\t\t\t\t\tviewport.WithHeight(20),\n\t\t\t\t),\n\t\t\t\tKeyMap: DefaultKeyMap(),\n\t\t\t\tHelp:   help.New(),\n\n\t\t\t\t// Modified fields\n\t\t\t\tstyles: Styles{},\n\t\t\t},\n\t\t},\n\t\t\"WithKeyMap\": {\n\t\t\topts: []Option{\n\t\t\t\tWithKeyMap(KeyMap{}),\n\t\t\t},\n\t\t\twant: Model{\n\t\t\t\t// Default fields\n\t\t\t\tcursor: 0,\n\t\t\t\tviewport: viewport.New(\n\t\t\t\t\tviewport.WithWidth(0),\n\t\t\t\t\tviewport.WithHeight(20),\n\t\t\t\t),\n\t\t\t\tHelp:   help.New(),\n\t\t\t\tstyles: DefaultStyles(),\n\n\t\t\t\t// Modified fields\n\t\t\t\tKeyMap: KeyMap{},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor name, tc := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc.want.UpdateViewport()\n\n\t\t\tgot := New(tc.opts...)\n\n\t\t\t// NOTE(@andreynering): Funcs have different references, so we need\n\t\t\t// to clear them out to compare the structs.\n\t\t\ttc.want.viewport.LeftGutterFunc = nil\n\t\t\tgot.viewport.LeftGutterFunc = nil\n\n\t\t\tif !reflect.DeepEqual(tc.want, got) {\n\t\t\t\tt.Errorf(\"\\n\\nwant %v\\n\\ngot %v\", tc.want, got)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestModel_FromValues(t *testing.T) {\n\tinput := \"foo1,bar1\\nfoo2,bar2\\nfoo3,bar3\"\n\ttable := New(WithColumns([]Column{{Title: \"Foo\"}, {Title: \"Bar\"}}))\n\ttable.FromValues(input, \",\")\n\n\tif len(table.rows) != 3 {\n\t\tt.Fatalf(\"expect table to have 3 rows but it has %d\", len(table.rows))\n\t}\n\n\texpect := []Row{\n\t\t{\"foo1\", \"bar1\"},\n\t\t{\"foo2\", \"bar2\"},\n\t\t{\"foo3\", \"bar3\"},\n\t}\n\tif !reflect.DeepEqual(table.rows, expect) {\n\t\tt.Fatalf(\"\\n\\nwant %v\\n\\ngot %v\", expect, table.rows)\n\t}\n}\n\nfunc TestModel_FromValues_WithTabSeparator(t *testing.T) {\n\tinput := \"foo1.\\tbar1\\nfoo,bar,baz\\tbar,2\"\n\ttable := New(WithColumns([]Column{{Title: \"Foo\"}, {Title: \"Bar\"}}))\n\ttable.FromValues(input, \"\\t\")\n\n\tif len(table.rows) != 2 {\n\t\tt.Fatalf(\"expect table to have 2 rows but it has %d\", len(table.rows))\n\t}\n\n\texpect := []Row{\n\t\t{\"foo1.\", \"bar1\"},\n\t\t{\"foo,bar,baz\", \"bar,2\"},\n\t}\n\tif !reflect.DeepEqual(table.rows, expect) {\n\t\tt.Fatalf(\"\\n\\nwant %v\\n\\ngot %v\", expect, table.rows)\n\t}\n}\n\nfunc TestModel_RenderRow(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\ttable    *Model\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname: \"simple row\",\n\t\t\ttable: &Model{\n\t\t\t\trows:   []Row{{\"Foooooo\", \"Baaaaar\", \"Baaaaaz\"}},\n\t\t\t\tcols:   testCols,\n\t\t\t\tstyles: Styles{Cell: lipgloss.NewStyle()},\n\t\t\t},\n\t\t\texpected: \"Foooooo   Baaaaar   Baaaaaz   \",\n\t\t},\n\t\t{\n\t\t\tname: \"simple row with truncations\",\n\t\t\ttable: &Model{\n\t\t\t\trows:   []Row{{\"Foooooooooo\", \"Baaaaaaaaar\", \"Quuuuuuuuux\"}},\n\t\t\t\tcols:   testCols,\n\t\t\t\tstyles: Styles{Cell: lipgloss.NewStyle()},\n\t\t\t},\n\t\t\texpected: \"Foooooooo…Baaaaaaaa…Quuuuuuuu…\",\n\t\t},\n\t\t{\n\t\t\tname: \"simple row avoiding truncations\",\n\t\t\ttable: &Model{\n\t\t\t\trows:   []Row{{\"Fooooooooo\", \"Baaaaaaaar\", \"Quuuuuuuux\"}},\n\t\t\t\tcols:   testCols,\n\t\t\t\tstyles: Styles{Cell: lipgloss.NewStyle()},\n\t\t\t},\n\t\t\texpected: \"FoooooooooBaaaaaaaarQuuuuuuuux\",\n\t\t},\n\t}\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\trow := tc.table.renderRow(0)\n\t\t\tif row != tc.expected {\n\t\t\t\tt.Fatalf(\"\\n\\nWant: \\n%s\\n\\nGot:  \\n%s\\n\", tc.expected, row)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestModel_RenderRow_AnsiWidth(t *testing.T) {\n\tvalue := \"\\x1b[31mABCDEFGH\\x1b[0m\"\n\ttable := &Model{\n\t\trows:   []Row{{value}},\n\t\tcols:   []Column{{Title: \"col1\", Width: 8}},\n\t\tstyles: Styles{Cell: lipgloss.NewStyle()},\n\t}\n\n\tgot := ansi.Strip(table.renderRow(0))\n\twant := \"ABCDEFGH\"\n\tif got != want {\n\t\tt.Fatalf(\"\\n\\nWant: \\n%s\\n\\nGot:  \\n%s\\n\", want, got)\n\t}\n}\n\nfunc TestTableAlignment(t *testing.T) {\n\tt.Run(\"No border\", func(t *testing.T) {\n\t\tbiscuits := New(\n\t\t\tWithWidth(59),\n\t\t\tWithHeight(5),\n\t\t\tWithColumns([]Column{\n\t\t\t\t{Title: \"Name\", Width: 25},\n\t\t\t\t{Title: \"Country of Origin\", Width: 16},\n\t\t\t\t{Title: \"Dunk-able\", Width: 12},\n\t\t\t}),\n\t\t\tWithRows([]Row{\n\t\t\t\t{\"Chocolate Digestives\", \"UK\", \"Yes\"},\n\t\t\t\t{\"Tim Tams\", \"Australia\", \"No\"},\n\t\t\t\t{\"Hobnobs\", \"UK\", \"Yes\"},\n\t\t\t}),\n\t\t)\n\t\tgot := ansiStrip(biscuits.View())\n\t\tgolden.RequireEqual(t, []byte(got))\n\t})\n\tt.Run(\"With border\", func(t *testing.T) {\n\t\tbaseStyle := lipgloss.NewStyle().\n\t\t\tBorderStyle(lipgloss.NormalBorder()).\n\t\t\tBorderForeground(lipgloss.Color(\"240\"))\n\n\t\ts := DefaultStyles()\n\t\ts.Header = s.Header.\n\t\t\tBorderStyle(lipgloss.NormalBorder()).\n\t\t\tBorderForeground(lipgloss.Color(\"240\")).\n\t\t\tBorderBottom(true).\n\t\t\tBold(false)\n\n\t\tbiscuits := New(\n\t\t\tWithWidth(59),\n\t\t\tWithHeight(5),\n\t\t\tWithColumns([]Column{\n\t\t\t\t{Title: \"Name\", Width: 25},\n\t\t\t\t{Title: \"Country of Origin\", Width: 16},\n\t\t\t\t{Title: \"Dunk-able\", Width: 12},\n\t\t\t}),\n\t\t\tWithRows([]Row{\n\t\t\t\t{\"Chocolate Digestives\", \"UK\", \"Yes\"},\n\t\t\t\t{\"Tim Tams\", \"Australia\", \"No\"},\n\t\t\t\t{\"Hobnobs\", \"UK\", \"Yes\"},\n\t\t\t}),\n\t\t\tWithStyles(s),\n\t\t)\n\t\tgot := ansiStrip(baseStyle.Render(biscuits.View()))\n\t\tgolden.RequireEqual(t, []byte(got))\n\t})\n}\n\nfunc ansiStrip(s string) string {\n\t// Replace all \\r\\n with \\n\n\ts = strings.ReplaceAll(s, \"\\r\\n\", \"\\n\")\n\treturn ansi.Strip(s)\n}\n\nfunc TestCursorNavigation(t *testing.T) {\n\ttests := map[string]struct {\n\t\trows   []Row\n\t\taction func(*Model)\n\t\twant   int\n\t}{\n\t\t\"New\": {\n\t\t\trows: []Row{\n\t\t\t\t{\"r1\"},\n\t\t\t\t{\"r2\"},\n\t\t\t\t{\"r3\"},\n\t\t\t},\n\t\t\taction: func(_ *Model) {},\n\t\t\twant:   0,\n\t\t},\n\t\t\"MoveDown\": {\n\t\t\trows: []Row{\n\t\t\t\t{\"r1\"},\n\t\t\t\t{\"r2\"},\n\t\t\t\t{\"r3\"},\n\t\t\t\t{\"r4\"},\n\t\t\t},\n\t\t\taction: func(t *Model) {\n\t\t\t\tt.MoveDown(2)\n\t\t\t},\n\t\t\twant: 2,\n\t\t},\n\t\t\"MoveUp\": {\n\t\t\trows: []Row{\n\t\t\t\t{\"r1\"},\n\t\t\t\t{\"r2\"},\n\t\t\t\t{\"r3\"},\n\t\t\t\t{\"r4\"},\n\t\t\t},\n\t\t\taction: func(t *Model) {\n\t\t\t\tt.cursor = 3\n\t\t\t\tt.MoveUp(2)\n\t\t\t},\n\t\t\twant: 1,\n\t\t},\n\t\t\"GotoBottom\": {\n\t\t\trows: []Row{\n\t\t\t\t{\"r1\"},\n\t\t\t\t{\"r2\"},\n\t\t\t\t{\"r3\"},\n\t\t\t\t{\"r4\"},\n\t\t\t},\n\t\t\taction: func(t *Model) {\n\t\t\t\tt.GotoBottom()\n\t\t\t},\n\t\t\twant: 3,\n\t\t},\n\t\t\"GotoTop\": {\n\t\t\trows: []Row{\n\t\t\t\t{\"r1\"},\n\t\t\t\t{\"r2\"},\n\t\t\t\t{\"r3\"},\n\t\t\t\t{\"r4\"},\n\t\t\t},\n\t\t\taction: func(t *Model) {\n\t\t\t\tt.cursor = 3\n\t\t\t\tt.GotoTop()\n\t\t\t},\n\t\t\twant: 0,\n\t\t},\n\t\t\"SetCursor\": {\n\t\t\trows: []Row{\n\t\t\t\t{\"r1\"},\n\t\t\t\t{\"r2\"},\n\t\t\t\t{\"r3\"},\n\t\t\t\t{\"r4\"},\n\t\t\t},\n\t\t\taction: func(t *Model) {\n\t\t\t\tt.SetCursor(2)\n\t\t\t},\n\t\t\twant: 2,\n\t\t},\n\t\t\"MoveDown with overflow\": {\n\t\t\trows: []Row{\n\t\t\t\t{\"r1\"},\n\t\t\t\t{\"r2\"},\n\t\t\t\t{\"r3\"},\n\t\t\t\t{\"r4\"},\n\t\t\t},\n\t\t\taction: func(t *Model) {\n\t\t\t\tt.MoveDown(5)\n\t\t\t},\n\t\t\twant: 3,\n\t\t},\n\t\t\"MoveUp with overflow\": {\n\t\t\trows: []Row{\n\t\t\t\t{\"r1\"},\n\t\t\t\t{\"r2\"},\n\t\t\t\t{\"r3\"},\n\t\t\t\t{\"r4\"},\n\t\t\t},\n\t\t\taction: func(t *Model) {\n\t\t\t\tt.cursor = 3\n\t\t\t\tt.MoveUp(5)\n\t\t\t},\n\t\t\twant: 0,\n\t\t},\n\t\t\"Blur does not stop movement\": {\n\t\t\trows: []Row{\n\t\t\t\t{\"r1\"},\n\t\t\t\t{\"r2\"},\n\t\t\t\t{\"r3\"},\n\t\t\t\t{\"r4\"},\n\t\t\t},\n\t\t\taction: func(t *Model) {\n\t\t\t\tt.Blur()\n\t\t\t\tt.MoveDown(2)\n\t\t\t},\n\t\t\twant: 2,\n\t\t},\n\t}\n\n\tfor name, tc := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttable := New(WithColumns(testCols), WithRows(tc.rows))\n\t\t\ttc.action(&table)\n\n\t\t\tif table.Cursor() != tc.want {\n\t\t\t\tt.Errorf(\"want %d, got %d\", tc.want, table.Cursor())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestModel_SetRows(t *testing.T) {\n\ttable := New(WithColumns(testCols))\n\n\tif len(table.rows) != 0 {\n\t\tt.Fatalf(\"want 0, got %d\", len(table.rows))\n\t}\n\n\ttable.SetRows([]Row{{\"r1\"}, {\"r2\"}})\n\n\tif len(table.rows) != 2 {\n\t\tt.Fatalf(\"want 2, got %d\", len(table.rows))\n\t}\n\n\twant := []Row{{\"r1\"}, {\"r2\"}}\n\tif !reflect.DeepEqual(table.rows, want) {\n\t\tt.Fatalf(\"\\n\\nwant %v\\n\\ngot %v\", want, table.rows)\n\t}\n}\n\nfunc TestModel_SetColumns(t *testing.T) {\n\ttable := New()\n\n\tif len(table.cols) != 0 {\n\t\tt.Fatalf(\"want 0, got %d\", len(table.cols))\n\t}\n\n\ttable.SetColumns([]Column{{Title: \"Foo\"}, {Title: \"Bar\"}})\n\n\tif len(table.cols) != 2 {\n\t\tt.Fatalf(\"want 2, got %d\", len(table.cols))\n\t}\n\n\twant := []Column{{Title: \"Foo\"}, {Title: \"Bar\"}}\n\tif !reflect.DeepEqual(table.cols, want) {\n\t\tt.Fatalf(\"\\n\\nwant %v\\n\\ngot %v\", want, table.cols)\n\t}\n}\n\nfunc TestModel_View(t *testing.T) {\n\ttests := map[string]struct {\n\t\tmodelFunc func() Model\n\t\tskip      bool\n\t}{\n\t\t\"Empty\": {\n\t\t\tmodelFunc: func() Model {\n\t\t\t\treturn New(\n\t\t\t\t\tWithWidth(60),\n\t\t\t\t\tWithHeight(21),\n\t\t\t\t)\n\t\t\t},\n\t\t},\n\t\t\"Single row and column\": {\n\t\t\tmodelFunc: func() Model {\n\t\t\t\treturn New(\n\t\t\t\t\tWithWidth(27),\n\t\t\t\t\tWithHeight(21),\n\t\t\t\t\tWithColumns([]Column{\n\t\t\t\t\t\t{Title: \"Name\", Width: 25},\n\t\t\t\t\t}),\n\t\t\t\t\tWithRows([]Row{\n\t\t\t\t\t\t{\"Chocolate Digestives\"},\n\t\t\t\t\t}),\n\t\t\t\t)\n\t\t\t},\n\t\t},\n\t\t\"Multiple rows and columns\": {\n\t\t\tmodelFunc: func() Model {\n\t\t\t\treturn New(\n\t\t\t\t\tWithWidth(59),\n\t\t\t\t\tWithHeight(21),\n\t\t\t\t\tWithColumns([]Column{\n\t\t\t\t\t\t{Title: \"Name\", Width: 25},\n\t\t\t\t\t\t{Title: \"Country of Origin\", Width: 16},\n\t\t\t\t\t\t{Title: \"Dunk-able\", Width: 12},\n\t\t\t\t\t}),\n\t\t\t\t\tWithRows([]Row{\n\t\t\t\t\t\t{\"Chocolate Digestives\", \"UK\", \"Yes\"},\n\t\t\t\t\t\t{\"Tim Tams\", \"Australia\", \"No\"},\n\t\t\t\t\t\t{\"Hobnobs\", \"UK\", \"Yes\"},\n\t\t\t\t\t}),\n\t\t\t\t)\n\t\t\t},\n\t\t},\n\t\t// TODO(fix): since the table height is tied to the viewport height, adding vertical padding to the headers' height directly increases the table height.\n\t\t\"Extra padding\": {\n\t\t\tmodelFunc: func() Model {\n\t\t\t\ts := DefaultStyles()\n\t\t\t\ts.Header = lipgloss.NewStyle().Padding(2, 2)\n\t\t\t\ts.Cell = lipgloss.NewStyle().Padding(2, 2)\n\n\t\t\t\treturn New(\n\t\t\t\t\tWithWidth(60),\n\t\t\t\t\tWithHeight(10),\n\t\t\t\t\tWithColumns([]Column{\n\t\t\t\t\t\t{Title: \"Name\", Width: 25},\n\t\t\t\t\t\t{Title: \"Country of Origin\", Width: 16},\n\t\t\t\t\t\t{Title: \"Dunk-able\", Width: 12},\n\t\t\t\t\t}),\n\t\t\t\t\tWithRows([]Row{\n\t\t\t\t\t\t{\"Chocolate Digestives\", \"UK\", \"Yes\"},\n\t\t\t\t\t\t{\"Tim Tams\", \"Australia\", \"No\"},\n\t\t\t\t\t\t{\"Hobnobs\", \"UK\", \"Yes\"},\n\t\t\t\t\t}),\n\t\t\t\t\tWithStyles(s),\n\t\t\t\t)\n\t\t\t},\n\t\t},\n\t\t\"No padding\": {\n\t\t\tmodelFunc: func() Model {\n\t\t\t\ts := DefaultStyles()\n\t\t\t\ts.Header = lipgloss.NewStyle()\n\t\t\t\ts.Cell = lipgloss.NewStyle()\n\n\t\t\t\treturn New(\n\t\t\t\t\tWithWidth(53),\n\t\t\t\t\tWithHeight(10),\n\t\t\t\t\tWithColumns([]Column{\n\t\t\t\t\t\t{Title: \"Name\", Width: 25},\n\t\t\t\t\t\t{Title: \"Country of Origin\", Width: 16},\n\t\t\t\t\t\t{Title: \"Dunk-able\", Width: 12},\n\t\t\t\t\t}),\n\t\t\t\t\tWithRows([]Row{\n\t\t\t\t\t\t{\"Chocolate Digestives\", \"UK\", \"Yes\"},\n\t\t\t\t\t\t{\"Tim Tams\", \"Australia\", \"No\"},\n\t\t\t\t\t\t{\"Hobnobs\", \"UK\", \"Yes\"},\n\t\t\t\t\t}),\n\t\t\t\t\tWithStyles(s),\n\t\t\t\t)\n\t\t\t},\n\t\t},\n\t\t// TODO(?): the total height is modified with bordered headers, however not with bordered cells. Is this expected/desired?\n\t\t\"Bordered headers\": {\n\t\t\tmodelFunc: func() Model {\n\t\t\t\treturn New(\n\t\t\t\t\tWithWidth(59),\n\t\t\t\t\tWithHeight(23),\n\t\t\t\t\tWithColumns([]Column{\n\t\t\t\t\t\t{Title: \"Name\", Width: 25},\n\t\t\t\t\t\t{Title: \"Country of Origin\", Width: 16},\n\t\t\t\t\t\t{Title: \"Dunk-able\", Width: 12},\n\t\t\t\t\t}),\n\t\t\t\t\tWithRows([]Row{\n\t\t\t\t\t\t{\"Chocolate Digestives\", \"UK\", \"Yes\"},\n\t\t\t\t\t\t{\"Tim Tams\", \"Australia\", \"No\"},\n\t\t\t\t\t\t{\"Hobnobs\", \"UK\", \"Yes\"},\n\t\t\t\t\t}),\n\t\t\t\t\tWithStyles(Styles{\n\t\t\t\t\t\tHeader: lipgloss.NewStyle().BorderStyle(lipgloss.NormalBorder()),\n\t\t\t\t\t}),\n\t\t\t\t)\n\t\t\t},\n\t\t},\n\t\t// TODO(fix): Headers are not horizontally aligned with cells due to the border adding width to the cells.\n\t\t\"Bordered cells\": {\n\t\t\tmodelFunc: func() Model {\n\t\t\t\treturn New(\n\t\t\t\t\tWithWidth(59),\n\t\t\t\t\tWithHeight(21),\n\t\t\t\t\tWithColumns([]Column{\n\t\t\t\t\t\t{Title: \"Name\", Width: 25},\n\t\t\t\t\t\t{Title: \"Country of Origin\", Width: 16},\n\t\t\t\t\t\t{Title: \"Dunk-able\", Width: 12},\n\t\t\t\t\t}),\n\t\t\t\t\tWithRows([]Row{\n\t\t\t\t\t\t{\"Chocolate Digestives\", \"UK\", \"Yes\"},\n\t\t\t\t\t\t{\"Tim Tams\", \"Australia\", \"No\"},\n\t\t\t\t\t\t{\"Hobnobs\", \"UK\", \"Yes\"},\n\t\t\t\t\t}),\n\t\t\t\t\tWithStyles(Styles{\n\t\t\t\t\t\tCell: lipgloss.NewStyle().BorderStyle(lipgloss.NormalBorder()),\n\t\t\t\t\t}),\n\t\t\t\t)\n\t\t\t},\n\t\t},\n\t\t\"Height greater than rows\": {\n\t\t\tmodelFunc: func() Model {\n\t\t\t\treturn New(\n\t\t\t\t\tWithWidth(59),\n\t\t\t\t\tWithHeight(6),\n\t\t\t\t\tWithColumns([]Column{\n\t\t\t\t\t\t{Title: \"Name\", Width: 25},\n\t\t\t\t\t\t{Title: \"Country of Origin\", Width: 16},\n\t\t\t\t\t\t{Title: \"Dunk-able\", Width: 12},\n\t\t\t\t\t}),\n\t\t\t\t\tWithRows([]Row{\n\t\t\t\t\t\t{\"Chocolate Digestives\", \"UK\", \"Yes\"},\n\t\t\t\t\t\t{\"Tim Tams\", \"Australia\", \"No\"},\n\t\t\t\t\t\t{\"Hobnobs\", \"UK\", \"Yes\"},\n\t\t\t\t\t}),\n\t\t\t\t)\n\t\t\t},\n\t\t},\n\t\t\"Height less than rows\": {\n\t\t\tmodelFunc: func() Model {\n\t\t\t\treturn New(\n\t\t\t\t\tWithWidth(59),\n\t\t\t\t\tWithHeight(2),\n\t\t\t\t\tWithColumns([]Column{\n\t\t\t\t\t\t{Title: \"Name\", Width: 25},\n\t\t\t\t\t\t{Title: \"Country of Origin\", Width: 16},\n\t\t\t\t\t\t{Title: \"Dunk-able\", Width: 12},\n\t\t\t\t\t}),\n\t\t\t\t\tWithRows([]Row{\n\t\t\t\t\t\t{\"Chocolate Digestives\", \"UK\", \"Yes\"},\n\t\t\t\t\t\t{\"Tim Tams\", \"Australia\", \"No\"},\n\t\t\t\t\t\t{\"Hobnobs\", \"UK\", \"Yes\"},\n\t\t\t\t\t}),\n\t\t\t\t)\n\t\t\t},\n\t\t},\n\t\t// TODO(fix): spaces are added to the right of the viewport to fill the width, but the headers end as though they are not aware of the width.\n\t\t\"Width greater than columns\": {\n\t\t\tmodelFunc: func() Model {\n\t\t\t\treturn New(\n\t\t\t\t\tWithWidth(80),\n\t\t\t\t\tWithHeight(21),\n\t\t\t\t\tWithColumns([]Column{\n\t\t\t\t\t\t{Title: \"Name\", Width: 25},\n\t\t\t\t\t\t{Title: \"Country of Origin\", Width: 16},\n\t\t\t\t\t\t{Title: \"Dunk-able\", Width: 12},\n\t\t\t\t\t}),\n\t\t\t\t\tWithRows([]Row{\n\t\t\t\t\t\t{\"Chocolate Digestives\", \"UK\", \"Yes\"},\n\t\t\t\t\t\t{\"Tim Tams\", \"Australia\", \"No\"},\n\t\t\t\t\t\t{\"Hobnobs\", \"UK\", \"Yes\"},\n\t\t\t\t\t}),\n\t\t\t\t)\n\t\t\t},\n\t\t},\n\t\t// TODO(fix): Setting the table width does not affect the total headers' width. Cells are wrapped.\n\t\t// \tHeaders are not affected. Truncation/resizing should match lipgloss.table functionality.\n\t\t\"Width less than columns\": {\n\t\t\tmodelFunc: func() Model {\n\t\t\t\treturn New(\n\t\t\t\t\tWithWidth(30),\n\t\t\t\t\tWithHeight(15),\n\t\t\t\t\tWithColumns([]Column{\n\t\t\t\t\t\t{Title: \"Name\", Width: 25},\n\t\t\t\t\t\t{Title: \"Country of Origin\", Width: 16},\n\t\t\t\t\t\t{Title: \"Dunk-able\", Width: 12},\n\t\t\t\t\t}),\n\t\t\t\t\tWithRows([]Row{\n\t\t\t\t\t\t{\"Chocolate Digestives\", \"UK\", \"Yes\"},\n\t\t\t\t\t\t{\"Tim Tams\", \"Australia\", \"No\"},\n\t\t\t\t\t\t{\"Hobnobs\", \"UK\", \"Yes\"},\n\t\t\t\t\t}),\n\t\t\t\t)\n\t\t\t},\n\t\t\tskip: true,\n\t\t},\n\t\t\"Modified viewport height\": {\n\t\t\tmodelFunc: func() Model {\n\t\t\t\tm := New(\n\t\t\t\t\tWithWidth(59),\n\t\t\t\t\tWithHeight(15),\n\t\t\t\t\tWithColumns([]Column{\n\t\t\t\t\t\t{Title: \"Name\", Width: 25},\n\t\t\t\t\t\t{Title: \"Country of Origin\", Width: 16},\n\t\t\t\t\t\t{Title: \"Dunk-able\", Width: 12},\n\t\t\t\t\t}),\n\t\t\t\t\tWithRows([]Row{\n\t\t\t\t\t\t{\"Chocolate Digestives\", \"UK\", \"Yes\"},\n\t\t\t\t\t\t{\"Tim Tams\", \"Australia\", \"No\"},\n\t\t\t\t\t\t{\"Hobnobs\", \"UK\", \"Yes\"},\n\t\t\t\t\t}),\n\t\t\t\t)\n\n\t\t\t\tm.viewport.SetHeight(2)\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t},\n\t}\n\n\tfor name, tc := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tif tc.skip {\n\t\t\t\tt.Skip()\n\t\t\t}\n\n\t\t\ttable := tc.modelFunc()\n\n\t\t\tgot := ansi.Strip(table.View())\n\n\t\t\tgolden.RequireEqual(t, []byte(got))\n\t\t})\n\t}\n}\n\n// TODO: Fix table to make this test will pass.\nfunc TestModel_View_CenteredInABox(t *testing.T) {\n\tt.Skip()\n\n\tboxStyle := lipgloss.NewStyle().\n\t\tBorderStyle(lipgloss.NormalBorder()).\n\t\tAlign(lipgloss.Center)\n\n\ttable := New(\n\t\tWithHeight(6),\n\t\tWithWidth(80),\n\t\tWithColumns([]Column{\n\t\t\t{Title: \"Name\", Width: 25},\n\t\t\t{Title: \"Country of Origin\", Width: 16},\n\t\t\t{Title: \"Dunk-able\", Width: 12},\n\t\t}),\n\t\tWithRows([]Row{\n\t\t\t{\"Chocolate Digestives\", \"UK\", \"Yes\"},\n\t\t\t{\"Tim Tams\", \"Australia\", \"No\"},\n\t\t\t{\"Hobnobs\", \"UK\", \"Yes\"},\n\t\t}),\n\t)\n\n\ttableView := ansi.Strip(table.View())\n\tgot := boxStyle.Render(tableView)\n\n\tgolden.RequireEqual(t, []byte(got))\n}\n"
  },
  {
    "path": "table/testdata/TestModel_View/Bordered_cells.golden",
    "content": "Name                     Country of Orig…Dunk-able   \n┌─────────────────────────┐┌────────────────┐┌────────────┐\n│Chocolate Digestives     ││UK              ││Yes         │\n└─────────────────────────┘└────────────────┘└────────────┘\n┌─────────────────────────┐┌────────────────┐┌────────────┐\n│Tim Tams                 ││Australia       ││No          │\n└─────────────────────────┘└────────────────┘└────────────┘\n┌─────────────────────────┐┌────────────────┐┌────────────┐\n│Hobnobs                  ││UK              ││Yes         │\n└─────────────────────────┘└────────────────┘└────────────┘\n                                                           \n                                                           \n                                                           \n                                                           \n                                                           \n                                                           \n                                                           \n                                                           \n                                                           \n                                                           \n                                                           "
  },
  {
    "path": "table/testdata/TestModel_View/Bordered_headers.golden",
    "content": "┌─────────────────────────┐┌────────────────┐┌────────────┐\n│Name                     ││Country of Orig…││Dunk-able   │\n└─────────────────────────┘└────────────────┘└────────────┘\nChocolate Digestives     UK              Yes               \nTim Tams                 Australia       No                \nHobnobs                  UK              Yes               \n                                                           \n                                                           \n                                                           \n                                                           \n                                                           \n                                                           \n                                                           \n                                                           \n                                                           \n                                                           \n                                                           \n                                                           \n                                                           \n                                                           \n                                                           \n                                                           \n                                                           \n                                                           \n                                                           "
  },
  {
    "path": "table/testdata/TestModel_View/Empty.golden",
    "content": "\n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            "
  },
  {
    "path": "table/testdata/TestModel_View/Extra_padding.golden",
    "content": "                                                                 \n                                                                 \n  Name                         Country of Orig…    Dunk-able     \n                                                                 \n                                                                 \n                                                            \n                                                            \n  Chocolate Digestives         UK                  Yes      \n                                                            \n                                                            \n                                                            \n                                                            \n  Tim Tams                     Australia           No       \n                                                            "
  },
  {
    "path": "table/testdata/TestModel_View/Height_greater_than_rows.golden",
    "content": " Name                       Country of Orig…  Dunk-able    \n Chocolate Digestives       UK                Yes          \n Tim Tams                   Australia         No           \n Hobnobs                    UK                Yes          \n                                                           \n                                                           "
  },
  {
    "path": "table/testdata/TestModel_View/Height_less_than_rows.golden",
    "content": " Name                       Country of Orig…  Dunk-able    \n Chocolate Digestives       UK                Yes          "
  },
  {
    "path": "table/testdata/TestModel_View/Modified_viewport_height.golden",
    "content": " Name                       Country of Orig…  Dunk-able    \n Chocolate Digestives       UK                Yes          \n Tim Tams                   Australia         No           "
  },
  {
    "path": "table/testdata/TestModel_View/Multiple_rows_and_columns.golden",
    "content": " Name                       Country of Orig…  Dunk-able    \n Chocolate Digestives       UK                Yes          \n Tim Tams                   Australia         No           \n Hobnobs                    UK                Yes          \n                                                           \n                                                           \n                                                           \n                                                           \n                                                           \n                                                           \n                                                           \n                                                           \n                                                           \n                                                           \n                                                           \n                                                           \n                                                           \n                                                           \n                                                           \n                                                           \n                                                           "
  },
  {
    "path": "table/testdata/TestModel_View/No_padding.golden",
    "content": "Name                     Country of Orig…Dunk-able   \nChocolate Digestives     UK              Yes         \nTim Tams                 Australia       No          \nHobnobs                  UK              Yes         \n                                                     \n                                                     \n                                                     \n                                                     \n                                                     \n                                                     "
  },
  {
    "path": "table/testdata/TestModel_View/Single_row_and_column.golden",
    "content": " Name                      \n Chocolate Digestives      \n                           \n                           \n                           \n                           \n                           \n                           \n                           \n                           \n                           \n                           \n                           \n                           \n                           \n                           \n                           \n                           \n                           \n                           \n                           "
  },
  {
    "path": "table/testdata/TestModel_View/Width_greater_than_columns.golden",
    "content": " Name                       Country of Orig…  Dunk-able    \n Chocolate Digestives       UK                Yes                               \n Tim Tams                   Australia         No                                \n Hobnobs                    UK                Yes                               \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                "
  },
  {
    "path": "table/testdata/TestModel_View/Width_less_than_columns.golden",
    "content": " Name                     Country of Origin    Dunk-able\n Chocolate Digestives     UK                   Yes\n Tim Tams                 Australia            No\n Hobnobs                  UK                   Yes\n                              \n                              \n                              \n                              \n                              \n                              \n                              \n                              \n                              \n                              \n\n\n\n                              \n                              \n                              \n                              "
  },
  {
    "path": "table/testdata/TestModel_View_CenteredInABox.golden",
    "content": "┌────────────────────────────────────────────────────────────────────────────────┐\n│           Name                       Country of Orig…  Dunk-able               │\n│           Chocolate Digestives       UK                Yes                     │\n│           Tim Tams                   Australia         No                      │\n│           Hobnobs                    UK                Yes                     │\n│                                                                                │\n│                                                                                │\n└────────────────────────────────────────────────────────────────────────────────┘"
  },
  {
    "path": "table/testdata/TestTableAlignment/No_border.golden",
    "content": " Name                       Country of Orig…  Dunk-able    \n Chocolate Digestives       UK                Yes          \n Tim Tams                   Australia         No           \n Hobnobs                    UK                Yes          \n                                                           "
  },
  {
    "path": "table/testdata/TestTableAlignment/With_border.golden",
    "content": "┌───────────────────────────────────────────────────────────┐\n│ Name                       Country of Orig…  Dunk-able    │\n│───────────────────────────────────────────────────────────│\n│ Chocolate Digestives       UK                Yes          │\n│ Tim Tams                   Australia         No           │\n│ Hobnobs                    UK                Yes          │\n│                                                           │\n└───────────────────────────────────────────────────────────┘"
  },
  {
    "path": "textarea/textarea.go",
    "content": "// Package textarea provides a multi-line text input component for Bubble Tea\n// applications.\npackage textarea\n\nimport (\n\t\"crypto/sha256\"\n\t\"fmt\"\n\t\"image/color\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\t\"unicode\"\n\n\t\"charm.land/bubbles/v2/cursor\"\n\t\"charm.land/bubbles/v2/internal/memoization\"\n\t\"charm.land/bubbles/v2/internal/runeutil\"\n\t\"charm.land/bubbles/v2/key\"\n\t\"charm.land/bubbles/v2/viewport\"\n\ttea \"charm.land/bubbletea/v2\"\n\t\"charm.land/lipgloss/v2\"\n\t\"github.com/atotto/clipboard\"\n\t\"github.com/charmbracelet/x/ansi\"\n\trw \"github.com/mattn/go-runewidth\"\n\t\"github.com/rivo/uniseg\"\n)\n\nconst (\n\tminHeight        = 1\n\tdefaultHeight    = 6\n\tdefaultWidth     = 40\n\tdefaultCharLimit = 0 // no limit\n\tdefaultMaxHeight = 99\n\tdefaultMaxWidth  = 500\n\n\t// XXX: in v2, make max lines dynamic and default max lines configurable.\n\tmaxLines = 10000\n)\n\n// Internal messages for clipboard operations.\ntype (\n\tpasteMsg    string\n\tpasteErrMsg struct{ error }\n)\n\n// KeyMap is the key bindings for different actions within the textarea.\ntype KeyMap struct {\n\tCharacterBackward       key.Binding\n\tCharacterForward        key.Binding\n\tDeleteAfterCursor       key.Binding\n\tDeleteBeforeCursor      key.Binding\n\tDeleteCharacterBackward key.Binding\n\tDeleteCharacterForward  key.Binding\n\tDeleteWordBackward      key.Binding\n\tDeleteWordForward       key.Binding\n\tInsertNewline           key.Binding\n\tLineEnd                 key.Binding\n\tLineNext                key.Binding\n\tLinePrevious            key.Binding\n\tLineStart               key.Binding\n\tPageUp                  key.Binding\n\tPageDown                key.Binding\n\tPaste                   key.Binding\n\tWordBackward            key.Binding\n\tWordForward             key.Binding\n\tInputBegin              key.Binding\n\tInputEnd                key.Binding\n\n\tUppercaseWordForward  key.Binding\n\tLowercaseWordForward  key.Binding\n\tCapitalizeWordForward key.Binding\n\n\tTransposeCharacterBackward key.Binding\n}\n\n// DefaultKeyMap returns the default set of key bindings for navigating and acting\n// upon the textarea.\nfunc DefaultKeyMap() KeyMap {\n\treturn KeyMap{\n\t\tCharacterForward:        key.NewBinding(key.WithKeys(\"right\", \"ctrl+f\"), key.WithHelp(\"right\", \"character forward\")),\n\t\tCharacterBackward:       key.NewBinding(key.WithKeys(\"left\", \"ctrl+b\"), key.WithHelp(\"left\", \"character backward\")),\n\t\tWordForward:             key.NewBinding(key.WithKeys(\"alt+right\", \"alt+f\"), key.WithHelp(\"alt+right\", \"word forward\")),\n\t\tWordBackward:            key.NewBinding(key.WithKeys(\"alt+left\", \"alt+b\"), key.WithHelp(\"alt+left\", \"word backward\")),\n\t\tLineNext:                key.NewBinding(key.WithKeys(\"down\", \"ctrl+n\"), key.WithHelp(\"down\", \"next line\")),\n\t\tLinePrevious:            key.NewBinding(key.WithKeys(\"up\", \"ctrl+p\"), key.WithHelp(\"up\", \"previous line\")),\n\t\tDeleteWordBackward:      key.NewBinding(key.WithKeys(\"alt+backspace\", \"ctrl+w\"), key.WithHelp(\"alt+backspace\", \"delete word backward\")),\n\t\tDeleteWordForward:       key.NewBinding(key.WithKeys(\"alt+delete\", \"alt+d\"), key.WithHelp(\"alt+delete\", \"delete word forward\")),\n\t\tDeleteAfterCursor:       key.NewBinding(key.WithKeys(\"ctrl+k\"), key.WithHelp(\"ctrl+k\", \"delete after cursor\")),\n\t\tDeleteBeforeCursor:      key.NewBinding(key.WithKeys(\"ctrl+u\"), key.WithHelp(\"ctrl+u\", \"delete before cursor\")),\n\t\tInsertNewline:           key.NewBinding(key.WithKeys(\"enter\", \"ctrl+m\"), key.WithHelp(\"enter\", \"insert newline\")),\n\t\tDeleteCharacterBackward: key.NewBinding(key.WithKeys(\"backspace\", \"ctrl+h\"), key.WithHelp(\"backspace\", \"delete character backward\")),\n\t\tDeleteCharacterForward:  key.NewBinding(key.WithKeys(\"delete\", \"ctrl+d\"), key.WithHelp(\"delete\", \"delete character forward\")),\n\t\tLineStart:               key.NewBinding(key.WithKeys(\"home\", \"ctrl+a\"), key.WithHelp(\"home\", \"line start\")),\n\t\tLineEnd:                 key.NewBinding(key.WithKeys(\"end\", \"ctrl+e\"), key.WithHelp(\"end\", \"line end\")),\n\t\tPageUp:                  key.NewBinding(key.WithKeys(\"pgup\"), key.WithHelp(\"pgup\", \"page up\")),\n\t\tPageDown:                key.NewBinding(key.WithKeys(\"pgdown\"), key.WithHelp(\"pgdown\", \"page down\")),\n\t\tPaste:                   key.NewBinding(key.WithKeys(\"ctrl+v\"), key.WithHelp(\"ctrl+v\", \"paste\")),\n\t\tInputBegin:              key.NewBinding(key.WithKeys(\"alt+<\", \"ctrl+home\"), key.WithHelp(\"alt+<\", \"input begin\")),\n\t\tInputEnd:                key.NewBinding(key.WithKeys(\"alt+>\", \"ctrl+end\"), key.WithHelp(\"alt+>\", \"input end\")),\n\n\t\tCapitalizeWordForward: key.NewBinding(key.WithKeys(\"alt+c\"), key.WithHelp(\"alt+c\", \"capitalize word forward\")),\n\t\tLowercaseWordForward:  key.NewBinding(key.WithKeys(\"alt+l\"), key.WithHelp(\"alt+l\", \"lowercase word forward\")),\n\t\tUppercaseWordForward:  key.NewBinding(key.WithKeys(\"alt+u\"), key.WithHelp(\"alt+u\", \"uppercase word forward\")),\n\n\t\tTransposeCharacterBackward: key.NewBinding(key.WithKeys(\"ctrl+t\"), key.WithHelp(\"ctrl+t\", \"transpose character backward\")),\n\t}\n}\n\n// LineInfo is a helper for keeping track of line information regarding\n// soft-wrapped lines.\ntype LineInfo struct {\n\t// Width is the number of columns in the line.\n\tWidth int\n\n\t// CharWidth is the number of characters in the line to account for\n\t// double-width runes.\n\tCharWidth int\n\n\t// Height is the number of rows in the line.\n\tHeight int\n\n\t// StartColumn is the index of the first column of the line.\n\tStartColumn int\n\n\t// ColumnOffset is the number of columns that the cursor is offset from the\n\t// start of the line.\n\tColumnOffset int\n\n\t// RowOffset is the number of rows that the cursor is offset from the start\n\t// of the line.\n\tRowOffset int\n\n\t// CharOffset is the number of characters that the cursor is offset\n\t// from the start of the line. This will generally be equivalent to\n\t// ColumnOffset, but will be different there are double-width runes before\n\t// the cursor.\n\tCharOffset int\n}\n\n// PromptInfo is a struct that can be used to store information about the\n// prompt.\ntype PromptInfo struct {\n\tLineNumber int\n\tFocused    bool\n}\n\n// CursorStyle is the style for real and virtual cursors.\ntype CursorStyle struct {\n\t// Style styles the cursor block.\n\t//\n\t// For real cursors, the foreground color set here will be used as the\n\t// cursor color.\n\tColor color.Color\n\n\t// Shape is the cursor shape. The following shapes are available:\n\t//\n\t// - tea.CursorBlock\n\t// - tea.CursorUnderline\n\t// - tea.CursorBar\n\t//\n\t// This is only used for real cursors.\n\tShape tea.CursorShape\n\n\t// CursorBlink determines whether or not the cursor should blink.\n\tBlink bool\n\n\t// BlinkSpeed is the speed at which the virtual cursor blinks. This has no\n\t// effect on real cursors as well as no effect if the cursor is set not to\n\t// [CursorBlink].\n\t//\n\t// By default, the blink speed is set to about 500ms.\n\tBlinkSpeed time.Duration\n}\n\n// Styles are the styles for the textarea, separated into focused and blurred\n// states. The appropriate styles will be chosen based on the focus state of\n// the textarea.\ntype Styles struct {\n\tFocused StyleState\n\tBlurred StyleState\n\tCursor  CursorStyle\n}\n\n// StyleState that will be applied to the text area.\n//\n// StyleState can be applied to focused and unfocused states to change the styles\n// depending on the focus state.\n//\n// For an introduction to styling with Lip Gloss see:\n// https://github.com/charmbracelet/lipgloss\ntype StyleState struct {\n\tBase             lipgloss.Style\n\tText             lipgloss.Style\n\tLineNumber       lipgloss.Style\n\tCursorLineNumber lipgloss.Style\n\tCursorLine       lipgloss.Style\n\tEndOfBuffer      lipgloss.Style\n\tPlaceholder      lipgloss.Style\n\tPrompt           lipgloss.Style\n}\n\nfunc (s StyleState) computedCursorLine() lipgloss.Style {\n\treturn s.CursorLine.Inherit(s.Base).Inline(true)\n}\n\nfunc (s StyleState) computedCursorLineNumber() lipgloss.Style {\n\treturn s.CursorLineNumber.\n\t\tInherit(s.CursorLine).\n\t\tInherit(s.Base).\n\t\tInline(true)\n}\n\nfunc (s StyleState) computedEndOfBuffer() lipgloss.Style {\n\treturn s.EndOfBuffer.Inherit(s.Base).Inline(true)\n}\n\nfunc (s StyleState) computedLineNumber() lipgloss.Style {\n\treturn s.LineNumber.Inherit(s.Base).Inline(true)\n}\n\nfunc (s StyleState) computedPlaceholder() lipgloss.Style {\n\treturn s.Placeholder.Inherit(s.Base).Inline(true)\n}\n\nfunc (s StyleState) computedPrompt() lipgloss.Style {\n\treturn s.Prompt.Inherit(s.Base).Inline(true)\n}\n\nfunc (s StyleState) computedText() lipgloss.Style {\n\treturn s.Text.Inherit(s.Base).Inline(true)\n}\n\n// line is the input to the text wrapping function. This is stored in a struct\n// so that it can be hashed and memoized.\ntype line struct {\n\trunes []rune\n\twidth int\n}\n\n// Hash returns a hash of the line.\nfunc (w line) Hash() string {\n\tv := fmt.Sprintf(\"%s:%d\", string(w.runes), w.width)\n\treturn fmt.Sprintf(\"%x\", sha256.Sum256([]byte(v)))\n}\n\n// Model is the Bubble Tea model for this text area element.\ntype Model struct {\n\tErr error\n\n\t// General settings.\n\tcache *memoization.MemoCache[line, [][]rune]\n\n\t// Prompt is printed at the beginning of each line.\n\t//\n\t// When changing the value of Prompt after the model has been\n\t// initialized, ensure that SetWidth() gets called afterwards.\n\t//\n\t// See also [SetPromptFunc] for a dynamic prompt.\n\tPrompt string\n\n\t// Placeholder is the text displayed when the user\n\t// hasn't entered anything yet.\n\tPlaceholder string\n\n\t// ShowLineNumbers, if enabled, causes line numbers to be printed\n\t// after the prompt.\n\tShowLineNumbers bool\n\n\t// EndOfBufferCharacter is displayed at the end of the input.\n\tEndOfBufferCharacter rune\n\n\t// KeyMap encodes the keybindings recognized by the widget.\n\tKeyMap KeyMap\n\n\t// virtualCursor manages the virtual cursor.\n\tvirtualCursor cursor.Model\n\n\t// CharLimit is the maximum number of characters this input element will\n\t// accept. If 0 or less, there's no limit.\n\tCharLimit int\n\n\t// MaxHeight is the maximum height of the text area in rows. If 0 or less,\n\t// there's no limit.\n\tMaxHeight int\n\n\t// MaxWidth is the maximum width of the text area in columns. If 0 or less,\n\t// there's no limit.\n\tMaxWidth int\n\n\t// Styling. Styles are defined in [Styles]. Use [SetStyles] and [GetStyles]\n\t// to work with this value publicly.\n\tstyles Styles\n\n\t// useVirtualCursor determines whether or not to use the virtual cursor.\n\t// Use [SetVirtualCursor] and [VirtualCursor] to work with this this\n\t// value publicly.\n\tuseVirtualCursor bool\n\n\t// If promptFunc is set, it replaces Prompt as a generator for\n\t// prompt strings at the beginning of each line.\n\tpromptFunc func(PromptInfo) string\n\n\t// promptWidth is the width of the prompt.\n\tpromptWidth int\n\n\t// width is the maximum number of characters that can be displayed at once.\n\t// If 0 or less this setting is ignored.\n\twidth int\n\n\t// height is the maximum number of lines that can be displayed at once. It\n\t// essentially treats the text field like a vertically scrolling viewport\n\t// if there are more lines than the permitted height.\n\theight int\n\n\t// Underlying text value.\n\tvalue [][]rune\n\n\t// focus indicates whether user input focus should be on this input\n\t// component. When false, ignore keyboard input and hide the cursor.\n\tfocus bool\n\n\t// Cursor column.\n\tcol int\n\n\t// Cursor row.\n\trow int\n\n\t// Last character offset, used to maintain state when the cursor is moved\n\t// vertically such that we can maintain the same navigating position.\n\tlastCharOffset int\n\n\t// viewport is the vertically-scrollable viewport of the multi-line text\n\t// input.\n\tviewport *viewport.Model\n\n\t// rune sanitizer for input.\n\trsan runeutil.Sanitizer\n}\n\n// New creates a new model with default settings.\nfunc New() Model {\n\tvp := viewport.New()\n\tvp.KeyMap = viewport.KeyMap{}\n\tcur := cursor.New()\n\n\tstyles := DefaultDarkStyles()\n\n\tm := Model{\n\t\tCharLimit:            defaultCharLimit,\n\t\tMaxHeight:            defaultMaxHeight,\n\t\tMaxWidth:             defaultMaxWidth,\n\t\tPrompt:               lipgloss.ThickBorder().Left + \" \",\n\t\tstyles:               styles,\n\t\tcache:                memoization.NewMemoCache[line, [][]rune](maxLines),\n\t\tEndOfBufferCharacter: ' ',\n\t\tShowLineNumbers:      true,\n\t\tuseVirtualCursor:     true,\n\t\tvirtualCursor:        cur,\n\t\tKeyMap:               DefaultKeyMap(),\n\n\t\tvalue: make([][]rune, minHeight, maxLines),\n\t\tfocus: false,\n\t\tcol:   0,\n\t\trow:   0,\n\n\t\tviewport: &vp,\n\t}\n\n\tm.SetHeight(defaultHeight)\n\tm.SetWidth(defaultWidth)\n\n\treturn m\n}\n\n// DefaultStyles returns the default styles for focused and blurred states for\n// the textarea.\nfunc DefaultStyles(isDark bool) Styles {\n\tlightDark := lipgloss.LightDark(isDark)\n\n\tvar s Styles\n\ts.Focused = StyleState{\n\t\tBase:             lipgloss.NewStyle(),\n\t\tCursorLine:       lipgloss.NewStyle().Background(lightDark(lipgloss.Color(\"255\"), lipgloss.Color(\"0\"))),\n\t\tCursorLineNumber: lipgloss.NewStyle().Foreground(lightDark(lipgloss.Color(\"240\"), lipgloss.Color(\"240\"))),\n\t\tEndOfBuffer:      lipgloss.NewStyle().Foreground(lightDark(lipgloss.Color(\"254\"), lipgloss.Color(\"0\"))),\n\t\tLineNumber:       lipgloss.NewStyle().Foreground(lightDark(lipgloss.Color(\"249\"), lipgloss.Color(\"7\"))),\n\t\tPlaceholder:      lipgloss.NewStyle().Foreground(lipgloss.Color(\"240\")),\n\t\tPrompt:           lipgloss.NewStyle().Foreground(lipgloss.Color(\"7\")),\n\t\tText:             lipgloss.NewStyle(),\n\t}\n\ts.Blurred = StyleState{\n\t\tBase:             lipgloss.NewStyle(),\n\t\tCursorLine:       lipgloss.NewStyle().Foreground(lightDark(lipgloss.Color(\"245\"), lipgloss.Color(\"7\"))),\n\t\tCursorLineNumber: lipgloss.NewStyle().Foreground(lightDark(lipgloss.Color(\"249\"), lipgloss.Color(\"7\"))),\n\t\tEndOfBuffer:      lipgloss.NewStyle().Foreground(lightDark(lipgloss.Color(\"254\"), lipgloss.Color(\"0\"))),\n\t\tLineNumber:       lipgloss.NewStyle().Foreground(lightDark(lipgloss.Color(\"249\"), lipgloss.Color(\"7\"))),\n\t\tPlaceholder:      lipgloss.NewStyle().Foreground(lipgloss.Color(\"240\")),\n\t\tPrompt:           lipgloss.NewStyle().Foreground(lipgloss.Color(\"7\")),\n\t\tText:             lipgloss.NewStyle().Foreground(lightDark(lipgloss.Color(\"245\"), lipgloss.Color(\"7\"))),\n\t}\n\ts.Cursor = CursorStyle{\n\t\tColor: lipgloss.Color(\"7\"),\n\t\tShape: tea.CursorBlock,\n\t\tBlink: true,\n\t}\n\treturn s\n}\n\n// DefaultLightStyles returns the default styles for a light background.\nfunc DefaultLightStyles() Styles {\n\treturn DefaultStyles(false)\n}\n\n// DefaultDarkStyles returns the default styles for a dark background.\nfunc DefaultDarkStyles() Styles {\n\treturn DefaultStyles(true)\n}\n\n// Styles returns the current styles for the textarea.\nfunc (m Model) Styles() Styles {\n\treturn m.styles\n}\n\n// SetStyles updates styling for the textarea.\nfunc (m *Model) SetStyles(s Styles) {\n\tm.styles = s\n\tm.updateVirtualCursorStyle()\n}\n\n// VirtualCursor returns whether or not the virtual cursor is enabled.\nfunc (m Model) VirtualCursor() bool {\n\treturn m.useVirtualCursor\n}\n\n// SetVirtualCursor sets whether or not to use the virtual cursor.\nfunc (m *Model) SetVirtualCursor(v bool) {\n\tm.useVirtualCursor = v\n\tm.updateVirtualCursorStyle()\n}\n\n// updateVirtualCursorStyle sets styling on the virtual cursor based on the\n// textarea's style settings.\nfunc (m *Model) updateVirtualCursorStyle() {\n\tif !m.useVirtualCursor {\n\t\tm.virtualCursor.SetMode(cursor.CursorHide)\n\t\treturn\n\t}\n\n\tm.virtualCursor.Style = lipgloss.NewStyle().Foreground(m.styles.Cursor.Color)\n\n\t// By default, the blink speed of the cursor is set to a default\n\t// internally.\n\tif m.styles.Cursor.Blink {\n\t\tif m.styles.Cursor.BlinkSpeed > 0 {\n\t\t\tm.virtualCursor.BlinkSpeed = m.styles.Cursor.BlinkSpeed\n\t\t}\n\t\tm.virtualCursor.SetMode(cursor.CursorBlink)\n\t\treturn\n\t}\n\tm.virtualCursor.SetMode(cursor.CursorStatic)\n}\n\n// SetValue sets the value of the text input.\nfunc (m *Model) SetValue(s string) {\n\tm.Reset()\n\tm.InsertString(s)\n}\n\n// InsertString inserts a string at the cursor position.\nfunc (m *Model) InsertString(s string) {\n\tm.insertRunesFromUserInput([]rune(s))\n}\n\n// InsertRune inserts a rune at the cursor position.\nfunc (m *Model) InsertRune(r rune) {\n\tm.insertRunesFromUserInput([]rune{r})\n}\n\n// insertRunesFromUserInput inserts runes at the current cursor position.\nfunc (m *Model) insertRunesFromUserInput(runes []rune) {\n\t// Clean up any special characters in the input provided by the\n\t// clipboard. This avoids bugs due to e.g. tab characters and\n\t// whatnot.\n\trunes = m.san().Sanitize(runes)\n\n\tif m.CharLimit > 0 {\n\t\tavailSpace := m.CharLimit - m.Length()\n\t\t// If the char limit's been reached, cancel.\n\t\tif availSpace <= 0 {\n\t\t\treturn\n\t\t}\n\t\t// If there's not enough space to paste the whole thing cut the pasted\n\t\t// runes down so they'll fit.\n\t\tif availSpace < len(runes) {\n\t\t\trunes = runes[:availSpace]\n\t\t}\n\t}\n\n\t// Split the input into lines.\n\tvar lines [][]rune\n\tlstart := 0\n\tfor i := range runes {\n\t\tif runes[i] == '\\n' {\n\t\t\t// Queue a line to become a new row in the text area below.\n\t\t\t// Beware to clamp the max capacity of the slice, to ensure no\n\t\t\t// data from different rows get overwritten when later edits\n\t\t\t// will modify this line.\n\t\t\tlines = append(lines, runes[lstart:i:i])\n\t\t\tlstart = i + 1\n\t\t}\n\t}\n\tif lstart <= len(runes) {\n\t\t// The last line did not end with a newline character.\n\t\t// Take it now.\n\t\tlines = append(lines, runes[lstart:])\n\t}\n\n\t// Obey the maximum line limit.\n\tif maxLines > 0 && len(m.value)+len(lines)-1 > maxLines {\n\t\tallowedHeight := max(0, maxLines-len(m.value)+1)\n\t\tlines = lines[:allowedHeight]\n\t}\n\n\tif len(lines) == 0 {\n\t\t// Nothing left to insert.\n\t\treturn\n\t}\n\n\t// Save the remainder of the original line at the current\n\t// cursor position.\n\ttail := make([]rune, len(m.value[m.row][m.col:]))\n\tcopy(tail, m.value[m.row][m.col:])\n\n\t// Paste the first line at the current cursor position.\n\tm.value[m.row] = append(m.value[m.row][:m.col], lines[0]...)\n\tm.col += len(lines[0])\n\n\tif numExtraLines := len(lines) - 1; numExtraLines > 0 {\n\t\t// Add the new lines.\n\t\t// We try to reuse the slice if there's already space.\n\t\tvar newGrid [][]rune\n\t\tif cap(m.value) >= len(m.value)+numExtraLines {\n\t\t\t// Can reuse the extra space.\n\t\t\tnewGrid = m.value[:len(m.value)+numExtraLines]\n\t\t} else {\n\t\t\t// No space left; need a new slice.\n\t\t\tnewGrid = make([][]rune, len(m.value)+numExtraLines)\n\t\t\tcopy(newGrid, m.value[:m.row+1])\n\t\t}\n\t\t// Add all the rows that were after the cursor in the original\n\t\t// grid at the end of the new grid.\n\t\tcopy(newGrid[m.row+1+numExtraLines:], m.value[m.row+1:])\n\t\tm.value = newGrid\n\t\t// Insert all the new lines in the middle.\n\t\tfor _, l := range lines[1:] {\n\t\t\tm.row++\n\t\t\tm.value[m.row] = l\n\t\t\tm.col = len(l)\n\t\t}\n\t}\n\n\t// Finally add the tail at the end of the last line inserted.\n\tm.value[m.row] = append(m.value[m.row], tail...)\n\n\tm.SetCursorColumn(m.col)\n}\n\n// Value returns the value of the text input.\nfunc (m Model) Value() string {\n\tif m.value == nil {\n\t\treturn \"\"\n\t}\n\n\tvar v strings.Builder\n\tfor _, l := range m.value {\n\t\tv.WriteString(string(l))\n\t\tv.WriteByte('\\n')\n\t}\n\n\treturn strings.TrimSuffix(v.String(), \"\\n\")\n}\n\n// Length returns the number of characters currently in the text input.\nfunc (m *Model) Length() int {\n\tvar l int\n\tfor _, row := range m.value {\n\t\tl += uniseg.StringWidth(string(row))\n\t}\n\t// We add len(m.value) to include the newline characters.\n\treturn l + len(m.value) - 1\n}\n\n// LineCount returns the number of lines that are currently in the text input.\nfunc (m *Model) LineCount() int {\n\treturn len(m.value)\n}\n\n// Line returns the 0-indexed row position of the cursor.\nfunc (m Model) Line() int {\n\treturn m.row\n}\n\n// Column returns the 0-indexed column position of the cursor.\nfunc (m Model) Column() int {\n\treturn m.col\n}\n\n// ScrollYOffset returns the Y offset (top row) index of the current view, which\n// can be used to calculate the current scroll position.\nfunc (m Model) ScrollYOffset() int {\n\treturn m.viewport.YOffset()\n}\n\n// ScrollPercent returns the amount of the textarea that is currently scrolled\n// through, clamped between 0 and 1.\nfunc (m Model) ScrollPercent() float64 {\n\treturn m.viewport.ScrollPercent()\n}\n\n// setCursorLineRelative moves the cursor by the given number of lines. Negative\n// values move the cursor up, positive values move the cursor down.\nfunc (m *Model) setCursorLineRelative(delta int) {\n\tif delta == 0 {\n\t\treturn\n\t}\n\n\tli := m.LineInfo()\n\tcharOffset := max(m.lastCharOffset, li.CharOffset)\n\tm.lastCharOffset = charOffset\n\n\t// 2 columns to account for the trailing space wrapping.\n\tconst trailingSpace = 2\n\n\tif delta > 0 { //nolint:nestif\n\t\t// Moving down.\n\t\tfor range delta {\n\t\t\tif li.RowOffset+1 >= li.Height && m.row < len(m.value)-1 {\n\t\t\t\tm.row++\n\t\t\t\tm.col = 0\n\t\t\t} else {\n\t\t\t\t// Move the cursor to the start of the next virtual line.\n\t\t\t\tm.col = min(li.StartColumn+li.Width+trailingSpace, len(m.value[m.row])-1)\n\t\t\t}\n\t\t\tli = m.LineInfo()\n\t\t}\n\t} else {\n\t\t// Moving up.\n\t\tfor range -delta {\n\t\t\tif li.RowOffset <= 0 && m.row > 0 {\n\t\t\t\tm.row--\n\t\t\t\tm.col = len(m.value[m.row])\n\t\t\t} else {\n\t\t\t\t// Move the cursor to the end of the previous line.\n\t\t\t\tm.col = li.StartColumn - trailingSpace\n\t\t\t}\n\t\t\tli = m.LineInfo()\n\t\t}\n\t}\n\n\tnli := m.LineInfo()\n\tm.col = nli.StartColumn\n\n\tif nli.Width <= 0 {\n\t\tm.repositionView()\n\t\treturn\n\t}\n\n\toffset := 0\n\tfor offset < charOffset {\n\t\tif m.row >= len(m.value) || m.col >= len(m.value[m.row]) || offset >= nli.CharWidth-1 {\n\t\t\tbreak\n\t\t}\n\t\toffset += rw.RuneWidth(m.value[m.row][m.col])\n\t\tm.col++\n\t}\n\tm.repositionView()\n}\n\n// CursorDown moves the cursor down by one line.\nfunc (m *Model) CursorDown() {\n\tm.setCursorLineRelative(1)\n}\n\n// CursorUp moves the cursor up by one line.\nfunc (m *Model) CursorUp() {\n\tm.setCursorLineRelative(-1)\n}\n\n// SetCursorColumn moves the cursor to the given position. If the position is\n// out of bounds the cursor will be moved to the start or end accordingly.\nfunc (m *Model) SetCursorColumn(col int) {\n\tm.col = clamp(col, 0, len(m.value[m.row]))\n\t// Any time that we move the cursor horizontally we need to reset the last\n\t// offset so that the horizontal position when navigating is adjusted.\n\tm.lastCharOffset = 0\n}\n\n// CursorStart moves the cursor to the start of the input field.\nfunc (m *Model) CursorStart() {\n\tm.SetCursorColumn(0)\n}\n\n// CursorEnd moves the cursor to the end of the input field.\nfunc (m *Model) CursorEnd() {\n\tm.SetCursorColumn(len(m.value[m.row]))\n}\n\n// Focused returns the focus state on the model.\nfunc (m Model) Focused() bool {\n\treturn m.focus\n}\n\n// activeStyle returns the appropriate set of styles to use depending on\n// whether the textarea is focused or blurred.\nfunc (m Model) activeStyle() *StyleState {\n\tif m.focus {\n\t\treturn &m.styles.Focused\n\t}\n\treturn &m.styles.Blurred\n}\n\n// Focus sets the focus state on the model. When the model is in focus it can\n// receive keyboard input and the cursor will be hidden.\nfunc (m *Model) Focus() tea.Cmd {\n\tm.focus = true\n\treturn m.virtualCursor.Focus()\n}\n\n// Blur removes the focus state on the model. When the model is blurred it can\n// not receive keyboard input and the cursor will be hidden.\nfunc (m *Model) Blur() {\n\tm.focus = false\n\tm.virtualCursor.Blur()\n}\n\n// Reset sets the input to its default state with no input.\nfunc (m *Model) Reset() {\n\tm.value = make([][]rune, minHeight, maxLines)\n\tm.col = 0\n\tm.row = 0\n\tm.viewport.GotoTop()\n\tm.SetCursorColumn(0)\n}\n\n// Word returns the word at the cursor position.\n// A word is delimited by spaces or line-breaks.\nfunc (m *Model) Word() string {\n\tline := m.value[m.row]\n\tcol := m.col - 1\n\n\tif col < 0 {\n\t\treturn \"\"\n\t}\n\n\t// If cursor is beyond the line, return empty string\n\tif col >= len(line) {\n\t\treturn \"\"\n\t}\n\n\t// If cursor is on a space, return empty string\n\tif unicode.IsSpace(line[col]) {\n\t\treturn \"\"\n\t}\n\n\t// Find the start of the word by moving left\n\tstart := col\n\tfor start > 0 && !unicode.IsSpace(line[start-1]) {\n\t\tstart--\n\t}\n\n\t// Find the end of the word by moving right\n\tend := col\n\tfor end < len(line) && !unicode.IsSpace(line[end]) {\n\t\tend++\n\t}\n\n\treturn string(line[start:end])\n}\n\n// san initializes or retrieves the rune sanitizer.\nfunc (m *Model) san() runeutil.Sanitizer {\n\tif m.rsan == nil {\n\t\t// Textinput has all its input on a single line so collapse\n\t\t// newlines/tabs to single spaces.\n\t\tm.rsan = runeutil.NewSanitizer()\n\t}\n\treturn m.rsan\n}\n\n// deleteBeforeCursor deletes all text before the cursor. Returns whether or\n// not the cursor blink should be reset.\nfunc (m *Model) deleteBeforeCursor() {\n\tm.value[m.row] = m.value[m.row][m.col:]\n\tm.SetCursorColumn(0)\n}\n\n// deleteAfterCursor deletes all text after the cursor. Returns whether or not\n// the cursor blink should be reset. If input is masked delete everything after\n// the cursor so as not to reveal word breaks in the masked input.\nfunc (m *Model) deleteAfterCursor() {\n\tm.value[m.row] = m.value[m.row][:m.col]\n\tm.SetCursorColumn(len(m.value[m.row]))\n}\n\n// transposeLeft exchanges the runes at the cursor and immediately\n// before. No-op if the cursor is at the beginning of the line.  If\n// the cursor is not at the end of the line yet, moves the cursor to\n// the right.\nfunc (m *Model) transposeLeft() {\n\tif m.col == 0 || len(m.value[m.row]) < 2 {\n\t\treturn\n\t}\n\tif m.col >= len(m.value[m.row]) {\n\t\tm.SetCursorColumn(m.col - 1)\n\t}\n\tm.value[m.row][m.col-1], m.value[m.row][m.col] = m.value[m.row][m.col], m.value[m.row][m.col-1]\n\tif m.col < len(m.value[m.row]) {\n\t\tm.SetCursorColumn(m.col + 1)\n\t}\n}\n\n// deleteWordLeft deletes the word left to the cursor. Returns whether or not\n// the cursor blink should be reset.\nfunc (m *Model) deleteWordLeft() {\n\tif m.col == 0 || len(m.value[m.row]) == 0 {\n\t\treturn\n\t}\n\n\t// Linter note: it's critical that we acquire the initial cursor position\n\t// here prior to altering it via SetCursor() below. As such, moving this\n\t// call into the corresponding if clause does not apply here.\n\toldCol := m.col\n\n\tm.SetCursorColumn(m.col - 1)\n\tfor unicode.IsSpace(m.value[m.row][m.col]) {\n\t\tif m.col <= 0 {\n\t\t\tbreak\n\t\t}\n\t\t// ignore series of whitespace before cursor\n\t\tm.SetCursorColumn(m.col - 1)\n\t}\n\n\tfor m.col > 0 {\n\t\tif !unicode.IsSpace(m.value[m.row][m.col]) {\n\t\t\tm.SetCursorColumn(m.col - 1)\n\t\t} else {\n\t\t\tif m.col > 0 {\n\t\t\t\t// keep the previous space\n\t\t\t\tm.SetCursorColumn(m.col + 1)\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif oldCol > len(m.value[m.row]) {\n\t\tm.value[m.row] = m.value[m.row][:m.col]\n\t} else {\n\t\tm.value[m.row] = append(m.value[m.row][:m.col], m.value[m.row][oldCol:]...)\n\t}\n}\n\n// deleteWordRight deletes the word right to the cursor.\nfunc (m *Model) deleteWordRight() {\n\tif m.col >= len(m.value[m.row]) || len(m.value[m.row]) == 0 {\n\t\treturn\n\t}\n\n\toldCol := m.col\n\n\tfor m.col < len(m.value[m.row]) && unicode.IsSpace(m.value[m.row][m.col]) {\n\t\t// ignore series of whitespace after cursor\n\t\tm.SetCursorColumn(m.col + 1)\n\t}\n\n\tfor m.col < len(m.value[m.row]) {\n\t\tif !unicode.IsSpace(m.value[m.row][m.col]) {\n\t\t\tm.SetCursorColumn(m.col + 1)\n\t\t} else {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif m.col > len(m.value[m.row]) {\n\t\tm.value[m.row] = m.value[m.row][:oldCol]\n\t} else {\n\t\tm.value[m.row] = append(m.value[m.row][:oldCol], m.value[m.row][m.col:]...)\n\t}\n\n\tm.SetCursorColumn(oldCol)\n}\n\n// characterRight moves the cursor one character to the right.\nfunc (m *Model) characterRight() {\n\tif m.col < len(m.value[m.row]) {\n\t\tm.SetCursorColumn(m.col + 1)\n\t} else {\n\t\tif m.row < len(m.value)-1 {\n\t\t\tm.row++\n\t\t\tm.CursorStart()\n\t\t}\n\t}\n}\n\n// characterLeft moves the cursor one character to the left.\n// If insideLine is set, the cursor is moved to the last\n// character in the previous line, instead of one past that.\nfunc (m *Model) characterLeft(insideLine bool) {\n\tif m.col == 0 && m.row != 0 {\n\t\tm.row--\n\t\tm.CursorEnd()\n\t\tif !insideLine {\n\t\t\treturn\n\t\t}\n\t}\n\tif m.col > 0 {\n\t\tm.SetCursorColumn(m.col - 1)\n\t}\n}\n\n// wordLeft moves the cursor one word to the left. Returns whether or not the\n// cursor blink should be reset. If input is masked, move input to the start\n// so as not to reveal word breaks in the masked input.\nfunc (m *Model) wordLeft() {\n\tfor {\n\t\tm.characterLeft(true /* insideLine */)\n\t\tif m.col < len(m.value[m.row]) && !unicode.IsSpace(m.value[m.row][m.col]) {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tfor m.col > 0 {\n\t\tif unicode.IsSpace(m.value[m.row][m.col-1]) {\n\t\t\tbreak\n\t\t}\n\t\tm.SetCursorColumn(m.col - 1)\n\t}\n}\n\n// wordRight moves the cursor one word to the right. Returns whether or not the\n// cursor blink should be reset. If the input is masked, move input to the end\n// so as not to reveal word breaks in the masked input.\nfunc (m *Model) wordRight() {\n\tm.doWordRight(func(int, int) { /* nothing */ })\n}\n\nfunc (m *Model) doWordRight(fn func(charIdx int, pos int)) {\n\t// Skip spaces forward.\n\tfor m.col >= len(m.value[m.row]) || unicode.IsSpace(m.value[m.row][m.col]) {\n\t\tif m.row == len(m.value)-1 && m.col == len(m.value[m.row]) {\n\t\t\t// End of text.\n\t\t\tbreak\n\t\t}\n\t\tm.characterRight()\n\t}\n\n\tcharIdx := 0\n\tfor m.col < len(m.value[m.row]) {\n\t\tif unicode.IsSpace(m.value[m.row][m.col]) {\n\t\t\tbreak\n\t\t}\n\t\tfn(charIdx, m.col)\n\t\tm.SetCursorColumn(m.col + 1)\n\t\tcharIdx++\n\t}\n}\n\n// uppercaseRight changes the word to the right to uppercase.\nfunc (m *Model) uppercaseRight() {\n\tm.doWordRight(func(_ int, i int) {\n\t\tm.value[m.row][i] = unicode.ToUpper(m.value[m.row][i])\n\t})\n}\n\n// lowercaseRight changes the word to the right to lowercase.\nfunc (m *Model) lowercaseRight() {\n\tm.doWordRight(func(_ int, i int) {\n\t\tm.value[m.row][i] = unicode.ToLower(m.value[m.row][i])\n\t})\n}\n\n// capitalizeRight changes the word to the right to title case.\nfunc (m *Model) capitalizeRight() {\n\tm.doWordRight(func(charIdx int, i int) {\n\t\tif charIdx == 0 {\n\t\t\tm.value[m.row][i] = unicode.ToTitle(m.value[m.row][i])\n\t\t}\n\t})\n}\n\n// LineInfo returns the number of characters from the start of the\n// (soft-wrapped) line and the (soft-wrapped) line width.\nfunc (m Model) LineInfo() LineInfo {\n\tgrid := m.memoizedWrap(m.value[m.row], m.width)\n\n\t// Find out which line we are currently on. This can be determined by the\n\t// m.col and counting the number of runes that we need to skip.\n\tvar counter int\n\tfor i, line := range grid {\n\t\t// We've found the line that we are on\n\t\tif counter+len(line) == m.col && i+1 < len(grid) {\n\t\t\t// We wrap around to the next line if we are at the end of the\n\t\t\t// previous line so that we can be at the very beginning of the row\n\t\t\treturn LineInfo{\n\t\t\t\tCharOffset:   0,\n\t\t\t\tColumnOffset: 0,\n\t\t\t\tHeight:       len(grid),\n\t\t\t\tRowOffset:    i + 1,\n\t\t\t\tStartColumn:  m.col,\n\t\t\t\tWidth:        len(grid[i+1]),\n\t\t\t\tCharWidth:    uniseg.StringWidth(string(line)),\n\t\t\t}\n\t\t}\n\n\t\tif counter+len(line) >= m.col {\n\t\t\treturn LineInfo{\n\t\t\t\tCharOffset:   uniseg.StringWidth(string(line[:max(0, m.col-counter)])),\n\t\t\t\tColumnOffset: m.col - counter,\n\t\t\t\tHeight:       len(grid),\n\t\t\t\tRowOffset:    i,\n\t\t\t\tStartColumn:  counter,\n\t\t\t\tWidth:        len(line),\n\t\t\t\tCharWidth:    uniseg.StringWidth(string(line)),\n\t\t\t}\n\t\t}\n\n\t\tcounter += len(line)\n\t}\n\treturn LineInfo{}\n}\n\n// repositionView repositions the view of the viewport based on the defined\n// scrolling behavior.\nfunc (m *Model) repositionView() {\n\tminimum := m.viewport.YOffset()\n\tmaximum := minimum + m.viewport.Height() - 1\n\tif row := m.cursorLineNumber(); row < minimum {\n\t\tm.viewport.ScrollUp(minimum - row)\n\t} else if row > maximum {\n\t\tm.viewport.ScrollDown(row - maximum)\n\t}\n}\n\n// Width returns the width of the textarea.\nfunc (m Model) Width() int {\n\treturn m.width\n}\n\n// MoveToBegin moves the cursor to the beginning of the input.\nfunc (m *Model) MoveToBegin() {\n\tm.row = 0\n\tm.SetCursorColumn(0)\n\tm.repositionView()\n}\n\n// MoveToEnd moves the cursor to the end of the input.\nfunc (m *Model) MoveToEnd() {\n\tm.row = len(m.value) - 1\n\tm.SetCursorColumn(len(m.value[m.row]))\n\tm.repositionView()\n}\n\n// PageUp moves the cursor up by one page. First call snaps to the first visible\n// line, subsequent calls move up by a full page.\nfunc (m *Model) PageUp() {\n\t// If not on the first visible line, snap to it.\n\tif offset := m.viewport.YOffset() - m.cursorLineNumber(); offset < 0 {\n\t\tm.setCursorLineRelative(offset)\n\t\treturn\n\t}\n\n\t// Already on first visible line, move up by a full page.\n\tm.setCursorLineRelative(-m.height)\n}\n\n// PageDown moves the cursor down by one page. First call snaps to the last\n// visible line, subsequent calls move down by a full page.\nfunc (m *Model) PageDown() {\n\t// If not on the last visible line, snap to it.\n\tif offset := m.cursorLineNumber() - m.viewport.YOffset(); offset < m.height-1 {\n\t\tm.setCursorLineRelative(m.height - 1 - offset)\n\t\treturn\n\t}\n\n\t// Already on last visible line, move down by a full page.\n\tm.setCursorLineRelative(m.height)\n}\n\n// SetWidth sets the width of the textarea to fit exactly within the given width.\n// This means that the textarea will account for the width of the prompt and\n// whether or not line numbers are being shown.\n//\n// Ensure that SetWidth is called after setting the Prompt and ShowLineNumbers,\n// It is important that the width of the textarea be exactly the given width\n// and no more.\nfunc (m *Model) SetWidth(w int) {\n\t// Update prompt width only if there is no prompt function as\n\t// [SetPromptFunc] updates the prompt width when it is called.\n\tif m.promptFunc == nil {\n\t\t// XXX: Do we even need this or can we calculate the prompt width\n\t\t// at render time?\n\t\tm.promptWidth = uniseg.StringWidth(m.Prompt)\n\t}\n\n\t// Add base style borders and padding to reserved outer width.\n\treservedOuter := m.activeStyle().Base.GetHorizontalFrameSize()\n\n\t// Add prompt width to reserved inner width.\n\treservedInner := m.promptWidth\n\n\t// Add line number width to reserved inner width.\n\tif m.ShowLineNumbers {\n\t\t// XXX: this was originally documented as needing \"1 cell\" but was,\n\t\t// in practice, effectively hardcoded to 2 cells. We can, and should,\n\t\t// reduce this to one gap and update the tests accordingly.\n\t\tconst gap = 2\n\n\t\t// Number of digits plus 1 cell for the margin.\n\t\treservedInner += numDigits(m.MaxHeight) + gap\n\t}\n\n\t// Input width must be at least one more than the reserved inner and outer\n\t// width. This gives us a minimum input width of 1.\n\tminWidth := reservedInner + reservedOuter + 1\n\tinputWidth := max(w, minWidth)\n\n\t// Input width must be no more than maximum width.\n\tif m.MaxWidth > 0 {\n\t\tinputWidth = min(inputWidth, m.MaxWidth)\n\t}\n\n\t// Since the width of the viewport and input area is dependent on the width of\n\t// borders, prompt and line numbers, we need to calculate it by subtracting\n\t// the reserved width from them.\n\n\tm.viewport.SetWidth(inputWidth - reservedOuter)\n\tm.width = inputWidth - reservedOuter - reservedInner\n}\n\n// SetPromptFunc supersedes the Prompt field and sets a dynamic prompt instead.\n//\n// If the function returns a prompt that is shorter than the specified\n// promptWidth, it will be padded to the left. If it returns a prompt that is\n// longer, display artifacts may occur; the caller is responsible for computing\n// an adequate promptWidth.\nfunc (m *Model) SetPromptFunc(promptWidth int, fn func(PromptInfo) string) {\n\tm.promptFunc = fn\n\tm.promptWidth = promptWidth\n}\n\n// Height returns the current height of the textarea.\nfunc (m Model) Height() int {\n\treturn m.height\n}\n\n// SetHeight sets the height of the textarea.\nfunc (m *Model) SetHeight(h int) {\n\tif m.MaxHeight > 0 {\n\t\tm.height = clamp(h, minHeight, m.MaxHeight)\n\t\tm.viewport.SetHeight(clamp(h, minHeight, m.MaxHeight))\n\t} else {\n\t\tm.height = max(h, minHeight)\n\t\tm.viewport.SetHeight(max(h, minHeight))\n\t}\n\n\tm.repositionView()\n}\n\n// Update is the Bubble Tea update loop.\nfunc (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {\n\tif !m.focus {\n\t\tm.virtualCursor.Blur()\n\t\treturn m, nil\n\t}\n\n\t// Used to determine if the cursor should blink.\n\toldRow, oldCol := m.cursorLineNumber(), m.col\n\n\tvar cmds []tea.Cmd\n\n\tif m.value[m.row] == nil {\n\t\tm.value[m.row] = make([]rune, 0)\n\t}\n\n\tif m.MaxHeight > 0 && m.MaxHeight != m.cache.Capacity() {\n\t\tm.cache = memoization.NewMemoCache[line, [][]rune](m.MaxHeight)\n\t}\n\n\tswitch msg := msg.(type) {\n\tcase tea.PasteMsg:\n\t\tm.insertRunesFromUserInput([]rune(msg.Content))\n\tcase tea.KeyPressMsg:\n\t\tswitch {\n\t\tcase key.Matches(msg, m.KeyMap.DeleteAfterCursor):\n\t\t\tm.col = clamp(m.col, 0, len(m.value[m.row]))\n\t\t\tif m.col >= len(m.value[m.row]) {\n\t\t\t\tm.mergeLineBelow(m.row)\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tm.deleteAfterCursor()\n\t\tcase key.Matches(msg, m.KeyMap.DeleteBeforeCursor):\n\t\t\tm.col = clamp(m.col, 0, len(m.value[m.row]))\n\t\t\tif m.col <= 0 {\n\t\t\t\tm.mergeLineAbove(m.row)\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tm.deleteBeforeCursor()\n\t\tcase key.Matches(msg, m.KeyMap.DeleteCharacterBackward):\n\t\t\tm.col = clamp(m.col, 0, len(m.value[m.row]))\n\t\t\tif m.col <= 0 {\n\t\t\t\tm.mergeLineAbove(m.row)\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif len(m.value[m.row]) > 0 {\n\t\t\t\tm.value[m.row] = append(m.value[m.row][:max(0, m.col-1)], m.value[m.row][m.col:]...)\n\t\t\t\tif m.col > 0 {\n\t\t\t\t\tm.SetCursorColumn(m.col - 1)\n\t\t\t\t}\n\t\t\t}\n\t\tcase key.Matches(msg, m.KeyMap.DeleteCharacterForward):\n\t\t\tif len(m.value[m.row]) > 0 && m.col < len(m.value[m.row]) {\n\t\t\t\tm.value[m.row] = slices.Delete(m.value[m.row], m.col, m.col+1)\n\t\t\t}\n\t\t\tif m.col >= len(m.value[m.row]) {\n\t\t\t\tm.mergeLineBelow(m.row)\n\t\t\t\tbreak\n\t\t\t}\n\t\tcase key.Matches(msg, m.KeyMap.DeleteWordBackward):\n\t\t\tif m.col <= 0 {\n\t\t\t\tm.mergeLineAbove(m.row)\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tm.deleteWordLeft()\n\t\tcase key.Matches(msg, m.KeyMap.DeleteWordForward):\n\t\t\tm.col = clamp(m.col, 0, len(m.value[m.row]))\n\t\t\tif m.col >= len(m.value[m.row]) {\n\t\t\t\tm.mergeLineBelow(m.row)\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tm.deleteWordRight()\n\t\tcase key.Matches(msg, m.KeyMap.InsertNewline):\n\t\t\tif m.MaxHeight > 0 && len(m.value) >= m.MaxHeight {\n\t\t\t\treturn m, nil\n\t\t\t}\n\t\t\tm.col = clamp(m.col, 0, len(m.value[m.row]))\n\t\t\tm.splitLine(m.row, m.col)\n\t\tcase key.Matches(msg, m.KeyMap.LineEnd):\n\t\t\tm.CursorEnd()\n\t\tcase key.Matches(msg, m.KeyMap.LineStart):\n\t\t\tm.CursorStart()\n\t\tcase key.Matches(msg, m.KeyMap.CharacterForward):\n\t\t\tm.characterRight()\n\t\tcase key.Matches(msg, m.KeyMap.LineNext):\n\t\t\tm.CursorDown()\n\t\tcase key.Matches(msg, m.KeyMap.WordForward):\n\t\t\tm.wordRight()\n\t\tcase key.Matches(msg, m.KeyMap.Paste):\n\t\t\treturn m, Paste\n\t\tcase key.Matches(msg, m.KeyMap.CharacterBackward):\n\t\t\tm.characterLeft(false /* insideLine */)\n\t\tcase key.Matches(msg, m.KeyMap.LinePrevious):\n\t\t\tm.CursorUp()\n\t\tcase key.Matches(msg, m.KeyMap.WordBackward):\n\t\t\tm.wordLeft()\n\t\tcase key.Matches(msg, m.KeyMap.InputBegin):\n\t\t\tm.MoveToBegin()\n\t\tcase key.Matches(msg, m.KeyMap.InputEnd):\n\t\t\tm.MoveToEnd()\n\t\tcase key.Matches(msg, m.KeyMap.PageUp):\n\t\t\tm.PageUp()\n\t\tcase key.Matches(msg, m.KeyMap.PageDown):\n\t\t\tm.PageDown()\n\t\tcase key.Matches(msg, m.KeyMap.LowercaseWordForward):\n\t\t\tm.lowercaseRight()\n\t\tcase key.Matches(msg, m.KeyMap.UppercaseWordForward):\n\t\t\tm.uppercaseRight()\n\t\tcase key.Matches(msg, m.KeyMap.CapitalizeWordForward):\n\t\t\tm.capitalizeRight()\n\t\tcase key.Matches(msg, m.KeyMap.TransposeCharacterBackward):\n\t\t\tm.transposeLeft()\n\n\t\tdefault:\n\t\t\tm.insertRunesFromUserInput([]rune(msg.Text))\n\t\t}\n\n\tcase pasteMsg:\n\t\tm.insertRunesFromUserInput([]rune(msg))\n\n\tcase pasteErrMsg:\n\t\tm.Err = msg\n\t}\n\n\t// Make sure we set the content of the viewport before updating it.\n\tview := m.view()\n\tm.viewport.SetContent(view)\n\tvp, cmd := m.viewport.Update(msg)\n\tm.viewport = &vp\n\tcmds = append(cmds, cmd)\n\n\tif m.useVirtualCursor {\n\t\tm.virtualCursor, cmd = m.virtualCursor.Update(msg)\n\n\t\t// If the cursor has moved, reset the blink state. This is a small UX\n\t\t// nuance that makes cursor movement obvious and feel snappy.\n\t\tnewRow, newCol := m.cursorLineNumber(), m.col\n\t\tif (newRow != oldRow || newCol != oldCol) && m.virtualCursor.Mode() == cursor.CursorBlink {\n\t\t\tm.virtualCursor.IsBlinked = false\n\t\t\tcmd = m.virtualCursor.Blink()\n\t\t}\n\t\tcmds = append(cmds, cmd)\n\t}\n\n\tm.repositionView()\n\n\treturn m, tea.Batch(cmds...)\n}\n\nfunc (m *Model) view() string {\n\tif len(m.Value()) == 0 && m.row == 0 && m.col == 0 && m.Placeholder != \"\" {\n\t\treturn m.placeholderView()\n\t}\n\tm.virtualCursor.TextStyle = m.activeStyle().computedCursorLine()\n\n\tvar (\n\t\ts                strings.Builder\n\t\tstyle            lipgloss.Style\n\t\tnewLines         int\n\t\twidestLineNumber int\n\t\tlineInfo         = m.LineInfo()\n\t\tstyles           = m.activeStyle()\n\t)\n\n\tdisplayLine := 0\n\tfor l, line := range m.value {\n\t\twrappedLines := m.memoizedWrap(line, m.width)\n\n\t\tif m.row == l {\n\t\t\tstyle = styles.computedCursorLine()\n\t\t} else {\n\t\t\tstyle = styles.computedText()\n\t\t}\n\n\t\tfor wl, wrappedLine := range wrappedLines {\n\t\t\tprompt := m.promptView(displayLine)\n\t\t\tprompt = styles.computedPrompt().Render(prompt)\n\t\t\ts.WriteString(style.Render(prompt))\n\t\t\tdisplayLine++\n\n\t\t\tvar ln string\n\t\t\tif m.ShowLineNumbers {\n\t\t\t\tif wl == 0 { // normal line\n\t\t\t\t\tisCursorLine := m.row == l\n\t\t\t\t\ts.WriteString(m.lineNumberView(l+1, isCursorLine))\n\t\t\t\t} else { // soft wrapped line\n\t\t\t\t\tisCursorLine := m.row == l\n\t\t\t\t\ts.WriteString(m.lineNumberView(-1, isCursorLine))\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Note the widest line number for padding purposes later.\n\t\t\tlnw := uniseg.StringWidth(ln)\n\t\t\tif lnw > widestLineNumber {\n\t\t\t\twidestLineNumber = lnw\n\t\t\t}\n\n\t\t\tstrwidth := uniseg.StringWidth(string(wrappedLine))\n\t\t\tpadding := m.width - strwidth\n\t\t\t// If the trailing space causes the line to be wider than the\n\t\t\t// width, we should not draw it to the screen since it will result\n\t\t\t// in an extra space at the end of the line which can look off when\n\t\t\t// the cursor line is showing.\n\t\t\tif strwidth > m.width {\n\t\t\t\t// The character causing the line to be wider than the width is\n\t\t\t\t// guaranteed to be a space since any other character would\n\t\t\t\t// have been wrapped.\n\t\t\t\twrappedLine = []rune(strings.TrimSuffix(string(wrappedLine), \" \"))\n\t\t\t\tpadding -= m.width - strwidth\n\t\t\t}\n\t\t\tif m.row == l && lineInfo.RowOffset == wl {\n\t\t\t\ts.WriteString(style.Render(string(wrappedLine[:lineInfo.ColumnOffset])))\n\t\t\t\tif m.col >= len(line) && lineInfo.CharOffset >= m.width {\n\t\t\t\t\tm.virtualCursor.SetChar(\" \")\n\t\t\t\t\ts.WriteString(m.virtualCursor.View())\n\t\t\t\t} else {\n\t\t\t\t\tm.virtualCursor.SetChar(string(wrappedLine[lineInfo.ColumnOffset]))\n\t\t\t\t\ts.WriteString(style.Render(m.virtualCursor.View()))\n\t\t\t\t\ts.WriteString(style.Render(string(wrappedLine[lineInfo.ColumnOffset+1:])))\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\ts.WriteString(style.Render(string(wrappedLine)))\n\t\t\t}\n\t\t\ts.WriteString(style.Render(strings.Repeat(\" \", max(0, padding))))\n\t\t\ts.WriteRune('\\n')\n\t\t\tnewLines++\n\t\t}\n\t}\n\n\t// Always show at least `m.Height` lines at all times.\n\t// To do this we can simply pad out a few extra new lines in the view.\n\tfor range m.height {\n\t\ts.WriteString(m.promptView(displayLine))\n\t\tdisplayLine++\n\n\t\t// Write end of buffer content\n\t\tleftGutter := string(m.EndOfBufferCharacter)\n\t\trightGapWidth := m.Width() - uniseg.StringWidth(leftGutter) + widestLineNumber\n\t\trightGap := strings.Repeat(\" \", max(0, rightGapWidth))\n\t\ts.WriteString(styles.computedEndOfBuffer().Render(leftGutter + rightGap))\n\t\ts.WriteRune('\\n')\n\t}\n\n\treturn s.String()\n}\n\n// View renders the text area in its current state.\nfunc (m Model) View() string {\n\t// XXX: This is a workaround for the case where the viewport hasn't\n\t// been initialized yet like during the initial render. In that case,\n\t// we need to render the view again because Update hasn't been called\n\t// yet to set the content of the viewport.\n\tm.viewport.SetContent(m.view())\n\tview := m.viewport.View()\n\tstyles := m.activeStyle()\n\treturn styles.Base.Render(view)\n}\n\n// promptView renders a single line of the prompt.\nfunc (m Model) promptView(displayLine int) (prompt string) {\n\tprompt = m.Prompt\n\tif m.promptFunc == nil {\n\t\treturn prompt\n\t}\n\tprompt = m.promptFunc(PromptInfo{\n\t\tLineNumber: displayLine,\n\t\tFocused:    m.focus,\n\t})\n\twidth := lipgloss.Width(prompt)\n\tif width < m.promptWidth {\n\t\tprompt = fmt.Sprintf(\"%*s%s\", m.promptWidth-width, \"\", prompt)\n\t}\n\n\treturn m.activeStyle().computedPrompt().Render(prompt)\n}\n\n// lineNumberView renders the line number.\n//\n// If the argument is less than 0, a space styled as a line number is returned\n// instead. Such cases are used for soft-wrapped lines.\n//\n// The second argument indicates whether this line number is for a 'cursorline'\n// line number.\nfunc (m Model) lineNumberView(n int, isCursorLine bool) (str string) {\n\tif !m.ShowLineNumbers {\n\t\treturn \"\"\n\t}\n\n\tif n <= 0 {\n\t\tstr = \" \"\n\t} else {\n\t\tstr = strconv.Itoa(n)\n\t}\n\n\t// XXX: is textStyle really necessary here?\n\ttextStyle := m.activeStyle().computedText()\n\tlineNumberStyle := m.activeStyle().computedLineNumber()\n\tif isCursorLine {\n\t\ttextStyle = m.activeStyle().computedCursorLine()\n\t\tlineNumberStyle = m.activeStyle().computedCursorLineNumber()\n\t}\n\n\t// Format line number dynamically based on the maximum number of lines.\n\tdigits := len(strconv.Itoa(m.MaxHeight))\n\tstr = fmt.Sprintf(\" %*v \", digits, str)\n\n\treturn textStyle.Render(lineNumberStyle.Render(str))\n}\n\n// placeholderView returns the prompt and placeholder, if any.\nfunc (m Model) placeholderView() string {\n\tvar (\n\t\ts      strings.Builder\n\t\tp      = m.Placeholder\n\t\tstyles = m.activeStyle()\n\t)\n\t// word wrap lines\n\tpwordwrap := ansi.Wordwrap(p, m.width, \"\")\n\t// hard wrap lines (handles lines that could not be word wrapped)\n\tpwrap := ansi.Hardwrap(pwordwrap, m.width, true)\n\t// split string by new lines\n\tplines := strings.Split(strings.TrimSpace(pwrap), \"\\n\")\n\n\tfor i := range m.height {\n\t\tisLineNumber := len(plines) > i\n\n\t\tlineStyle := styles.computedPlaceholder()\n\t\tif len(plines) > i {\n\t\t\tlineStyle = styles.computedCursorLine()\n\t\t}\n\n\t\t// render prompt\n\t\tprompt := m.promptView(i)\n\t\tprompt = styles.computedPrompt().Render(prompt)\n\t\ts.WriteString(lineStyle.Render(prompt))\n\n\t\t// when show line numbers enabled:\n\t\t// - render line number for only the cursor line\n\t\t// - indent other placeholder lines\n\t\t// this is consistent with vim with line numbers enabled\n\t\tif m.ShowLineNumbers {\n\t\t\tvar ln int\n\n\t\t\tswitch {\n\t\t\tcase i == 0:\n\t\t\t\tln = i + 1\n\t\t\t\tfallthrough\n\t\t\tcase len(plines) > i:\n\t\t\t\ts.WriteString(m.lineNumberView(ln, isLineNumber))\n\t\t\tdefault:\n\t\t\t}\n\t\t}\n\n\t\tswitch {\n\t\t// first line\n\t\tcase i == 0:\n\t\t\t// first character of first line as cursor with character\n\t\t\tm.virtualCursor.TextStyle = styles.computedPlaceholder()\n\n\t\t\tch, rest, _, _ := uniseg.FirstGraphemeClusterInString(plines[0], 0)\n\t\t\tm.virtualCursor.SetChar(ch)\n\t\t\ts.WriteString(lineStyle.Render(m.virtualCursor.View()))\n\n\t\t\t// the rest of the first line\n\t\t\ts.WriteString(lineStyle.Render(styles.computedPlaceholder().Render(rest)))\n\n\t\t\t// extend the first line with spaces to fill the width, so that\n\t\t\t// the entire line is filled when cursorline is enabled.\n\t\t\tgap := strings.Repeat(\" \", max(0, m.width-lipgloss.Width(plines[0])))\n\t\t\ts.WriteString(lineStyle.Render(gap))\n\t\t// remaining lines\n\t\tcase len(plines) > i:\n\t\t\t// current line placeholder text\n\t\t\tif len(plines) > i {\n\t\t\t\tplaceholderLine := plines[i]\n\t\t\t\tgap := strings.Repeat(\" \", max(0, m.width-uniseg.StringWidth(plines[i])))\n\t\t\t\ts.WriteString(lineStyle.Render(placeholderLine + gap))\n\t\t\t}\n\t\tdefault:\n\t\t\t// end of line buffer character\n\t\t\teob := styles.computedEndOfBuffer().Render(string(m.EndOfBufferCharacter))\n\t\t\ts.WriteString(eob)\n\t\t}\n\n\t\t// terminate with new line\n\t\ts.WriteRune('\\n')\n\t}\n\n\tm.viewport.SetContent(s.String())\n\treturn styles.Base.Render(m.viewport.View())\n}\n\n// Blink returns the blink command for the virtual cursor.\nfunc Blink() tea.Msg {\n\treturn cursor.Blink()\n}\n\n// Cursor returns a [tea.Cursor] for rendering a real cursor in a Bubble Tea\n// program. This requires that [Model.VirtualCursor] is set to false.\n//\n// Note that you will almost certainly also need to adjust the offset cursor\n// position per the textarea's per the textarea's position in the terminal.\n//\n// Example:\n//\n//\t// In your top-level View function:\n//\tf := tea.NewFrame(m.textarea.View())\n//\tf.Cursor = m.textarea.Cursor()\n//\tf.Cursor.Position.X += offsetX\n//\tf.Cursor.Position.Y += offsetY\nfunc (m Model) Cursor() *tea.Cursor {\n\tif m.useVirtualCursor || !m.Focused() {\n\t\treturn nil\n\t}\n\n\tlineInfo := m.LineInfo()\n\tw := lipgloss.Width\n\tbaseStyle := m.activeStyle().Base\n\n\txOffset := lineInfo.CharOffset +\n\t\tw(m.promptView(0)) +\n\t\tw(m.lineNumberView(0, false)) +\n\t\tbaseStyle.GetMarginLeft() +\n\t\tbaseStyle.GetPaddingLeft() +\n\t\tbaseStyle.GetBorderLeftSize()\n\n\tyOffset := m.cursorLineNumber() -\n\t\tm.viewport.YOffset() +\n\t\tbaseStyle.GetMarginTop() +\n\t\tbaseStyle.GetPaddingTop() +\n\t\tbaseStyle.GetBorderTopSize()\n\n\tc := tea.NewCursor(xOffset, yOffset)\n\tc.Blink = m.styles.Cursor.Blink\n\tc.Color = m.styles.Cursor.Color\n\tc.Shape = m.styles.Cursor.Shape\n\treturn c\n}\n\nfunc (m Model) memoizedWrap(runes []rune, width int) [][]rune {\n\tinput := line{runes: runes, width: width}\n\tif v, ok := m.cache.Get(input); ok {\n\t\treturn v\n\t}\n\tv := wrap(runes, width)\n\tm.cache.Set(input, v)\n\treturn v\n}\n\n// cursorLineNumber returns the line number that the cursor is on.\n// This accounts for soft wrapped lines.\nfunc (m Model) cursorLineNumber() int {\n\tline := 0\n\tfor i := range m.row {\n\t\t// Calculate the number of lines that the current line will be split\n\t\t// into.\n\t\tline += len(m.memoizedWrap(m.value[i], m.width))\n\t}\n\tline += m.LineInfo().RowOffset\n\treturn line\n}\n\n// mergeLineBelow merges the current line the cursor is on with the line below.\nfunc (m *Model) mergeLineBelow(row int) {\n\tif row >= len(m.value)-1 {\n\t\treturn\n\t}\n\n\t// To perform a merge, we will need to combine the two lines and then\n\tm.value[row] = append(m.value[row], m.value[row+1]...)\n\n\t// Shift all lines up by one\n\tfor i := row + 1; i < len(m.value)-1; i++ {\n\t\tm.value[i] = m.value[i+1]\n\t}\n\n\t// And, remove the last line\n\tif len(m.value) > 0 {\n\t\tm.value = m.value[:len(m.value)-1]\n\t}\n}\n\n// mergeLineAbove merges the current line the cursor is on with the line above.\nfunc (m *Model) mergeLineAbove(row int) {\n\tif row <= 0 {\n\t\treturn\n\t}\n\n\tm.col = len(m.value[row-1])\n\tm.row = m.row - 1\n\n\t// To perform a merge, we will need to combine the two lines and then\n\tm.value[row-1] = append(m.value[row-1], m.value[row]...)\n\n\t// Shift all lines up by one\n\tfor i := row; i < len(m.value)-1; i++ {\n\t\tm.value[i] = m.value[i+1]\n\t}\n\n\t// And, remove the last line\n\tif len(m.value) > 0 {\n\t\tm.value = m.value[:len(m.value)-1]\n\t}\n}\n\nfunc (m *Model) splitLine(row, col int) {\n\t// To perform a split, take the current line and keep the content before\n\t// the cursor, take the content after the cursor and make it the content of\n\t// the line underneath, and shift the remaining lines down by one\n\thead, tailSrc := m.value[row][:col], m.value[row][col:]\n\ttail := make([]rune, len(tailSrc))\n\tcopy(tail, tailSrc)\n\n\tm.value = append(m.value[:row+1], m.value[row:]...)\n\n\tm.value[row] = head\n\tm.value[row+1] = tail\n\n\tm.col = 0\n\tm.row++\n}\n\n// Paste is a command for pasting from the clipboard into the text input.\nfunc Paste() tea.Msg {\n\tstr, err := clipboard.ReadAll()\n\tif err != nil {\n\t\treturn pasteErrMsg{err}\n\t}\n\treturn pasteMsg(str)\n}\n\nfunc wrap(runes []rune, width int) [][]rune {\n\tvar (\n\t\tlines  = [][]rune{{}}\n\t\tword   = []rune{}\n\t\trow    int\n\t\tspaces int\n\t)\n\n\t// Word wrap the runes\n\tfor _, r := range runes {\n\t\tif unicode.IsSpace(r) {\n\t\t\tspaces++\n\t\t} else {\n\t\t\tword = append(word, r)\n\t\t}\n\n\t\tif spaces > 0 { //nolint:nestif\n\t\t\tif uniseg.StringWidth(string(lines[row]))+uniseg.StringWidth(string(word))+spaces > width {\n\t\t\t\trow++\n\t\t\t\tlines = append(lines, []rune{})\n\t\t\t\tlines[row] = append(lines[row], word...)\n\t\t\t\tlines[row] = append(lines[row], repeatSpaces(spaces)...)\n\t\t\t\tspaces = 0\n\t\t\t\tword = nil\n\t\t\t} else {\n\t\t\t\tlines[row] = append(lines[row], word...)\n\t\t\t\tlines[row] = append(lines[row], repeatSpaces(spaces)...)\n\t\t\t\tspaces = 0\n\t\t\t\tword = nil\n\t\t\t}\n\t\t} else {\n\t\t\t// If the last character is a double-width rune, then we may not be able to add it to this line\n\t\t\t// as it might cause us to go past the width.\n\t\t\tlastCharLen := rw.RuneWidth(word[len(word)-1])\n\t\t\tif uniseg.StringWidth(string(word))+lastCharLen > width {\n\t\t\t\t// If the current line has any content, let's move to the next\n\t\t\t\t// line because the current word fills up the entire line.\n\t\t\t\tif len(lines[row]) > 0 {\n\t\t\t\t\trow++\n\t\t\t\t\tlines = append(lines, []rune{})\n\t\t\t\t}\n\t\t\t\tlines[row] = append(lines[row], word...)\n\t\t\t\tword = nil\n\t\t\t}\n\t\t}\n\t}\n\n\tif uniseg.StringWidth(string(lines[row]))+uniseg.StringWidth(string(word))+spaces >= width {\n\t\tlines = append(lines, []rune{})\n\t\tlines[row+1] = append(lines[row+1], word...)\n\t\t// We add an extra space at the end of the line to account for the\n\t\t// trailing space at the end of the previous soft-wrapped lines so that\n\t\t// behaviour when navigating is consistent and so that we don't need to\n\t\t// continually add edges to handle the last line of the wrapped input.\n\t\tspaces++\n\t\tlines[row+1] = append(lines[row+1], repeatSpaces(spaces)...)\n\t} else {\n\t\tlines[row] = append(lines[row], word...)\n\t\tspaces++\n\t\tlines[row] = append(lines[row], repeatSpaces(spaces)...)\n\t}\n\n\treturn lines\n}\n\nfunc repeatSpaces(n int) []rune {\n\treturn []rune(strings.Repeat(string(' '), n))\n}\n\n// numDigits returns the number of digits in an integer.\nfunc numDigits(n int) int {\n\tif n == 0 {\n\t\treturn 1\n\t}\n\tcount := 0\n\tnum := abs(n)\n\tfor num > 0 {\n\t\tcount++\n\t\tnum /= 10\n\t}\n\treturn count\n}\n\nfunc clamp(v, low, high int) int {\n\tif high < low {\n\t\tlow, high = high, low\n\t}\n\treturn min(high, max(low, v))\n}\n\nfunc abs(n int) int {\n\tif n < 0 {\n\t\treturn -n\n\t}\n\treturn n\n}\n"
  },
  {
    "path": "textarea/textarea_test.go",
    "content": "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/v2\"\n\t\"github.com/MakeNowJust/heredoc\"\n\t\"github.com/charmbracelet/x/ansi\"\n)\n\nfunc TestVerticalScrolling(t *testing.T) {\n\ttextarea := newTextArea()\n\ttextarea.Prompt = \"\"\n\ttextarea.ShowLineNumbers = false\n\ttextarea.SetHeight(1)\n\ttextarea.SetWidth(20)\n\ttextarea.CharLimit = 100\n\n\ttextarea, _ = textarea.Update(nil)\n\n\tinput := \"This is a really long line that should wrap around the text area.\"\n\n\tfor _, k := range input {\n\t\ttextarea, _ = textarea.Update(keyPress(k))\n\t}\n\n\tview := textarea.View()\n\n\t// The view should contain the end of \"line\" of the input.\n\tif !strings.Contains(view, \"the text area.\") {\n\t\tt.Log(view)\n\t\tt.Error(\"Text area did not render the input\")\n\t}\n\n\t// But we should be able to scroll to see the next line.\n\t// Let's scroll down for each line to view the full input.\n\tlines := []string{\n\t\t\"This is a really\",\n\t\t\"long line that\",\n\t\t\"should wrap around\",\n\t\t\"the text area.\",\n\t}\n\ttextarea.viewport.GotoTop()\n\tfor _, line := range lines {\n\t\tview = textarea.View()\n\t\tif !strings.Contains(view, line) {\n\t\t\tt.Log(view)\n\t\t\tt.Error(\"Text area did not render the correct scrolled input\")\n\t\t}\n\t\ttextarea.viewport.ScrollDown(1)\n\t}\n}\n\nfunc TestWordWrapOverflowing(t *testing.T) {\n\t// An interesting edge case is when the user enters many words that fill up\n\t// the text area and then goes back up and inserts a few words which causes\n\t// a cascading wrap and causes an overflow of the last line.\n\t//\n\t// In this case, we should not let the user insert more words if, after the\n\t// entire wrap is complete, the last line is overflowing.\n\ttextarea := newTextArea()\n\n\ttextarea.SetHeight(3)\n\ttextarea.SetWidth(20)\n\ttextarea.CharLimit = 500\n\n\ttextarea, _ = textarea.Update(nil)\n\n\tinput := \"Testing Testing Testing Testing Testing Testing Testing Testing\"\n\n\tfor _, k := range input {\n\t\ttextarea, _ = textarea.Update(keyPress(k))\n\t\ttextarea.View()\n\t}\n\n\t// We have essentially filled the text area with input.\n\t// Let's see if we can cause wrapping to overflow the last line.\n\ttextarea.row = 0\n\ttextarea.col = 0\n\n\tinput = \"Testing\"\n\n\tfor _, k := range input {\n\t\ttextarea, _ = textarea.Update(keyPress(k))\n\t\ttextarea.View()\n\t}\n\n\tlastLineWidth := textarea.LineInfo().Width\n\tif lastLineWidth > 20 {\n\t\tt.Log(lastLineWidth)\n\t\tt.Log(textarea.View())\n\t\tt.Fail()\n\t}\n}\n\nfunc TestValueSoftWrap(t *testing.T) {\n\ttextarea := newTextArea()\n\ttextarea.SetWidth(16)\n\ttextarea.SetHeight(10)\n\ttextarea.CharLimit = 500\n\n\ttextarea, _ = textarea.Update(nil)\n\n\tinput := \"Testing Testing Testing Testing Testing Testing Testing Testing\"\n\n\tfor _, k := range []rune(input) {\n\t\ttextarea, _ = textarea.Update(keyPress(k))\n\t\ttextarea.View()\n\t}\n\n\tvalue := textarea.Value()\n\tif value != input {\n\t\tt.Log(value)\n\t\tt.Log(input)\n\t\tt.Fatal(\"The text area does not have the correct value\")\n\t}\n}\n\nfunc TestSetValue(t *testing.T) {\n\ttextarea := newTextArea()\n\ttextarea.SetValue(strings.Join([]string{\"Foo\", \"Bar\", \"Baz\"}, \"\\n\"))\n\n\tif textarea.row != 2 && textarea.col != 3 {\n\t\tt.Log(textarea.row, textarea.col)\n\t\tt.Fatal(\"Cursor Should be on row 2 column 3 after inserting 2 new lines\")\n\t}\n\n\tvalue := textarea.Value()\n\tif value != \"Foo\\nBar\\nBaz\" {\n\t\tt.Fatal(\"Value should be Foo\\nBar\\nBaz\")\n\t}\n\n\t// SetValue should reset text area\n\ttextarea.SetValue(\"Test\")\n\tvalue = textarea.Value()\n\tif value != \"Test\" {\n\t\tt.Log(value)\n\t\tt.Fatal(\"Text area was not reset when SetValue() was called\")\n\t}\n}\n\nfunc TestInsertString(t *testing.T) {\n\ttextarea := newTextArea()\n\n\t// Insert some text\n\tinput := \"foo baz\"\n\n\tfor _, k := range []rune(input) {\n\t\ttextarea, _ = textarea.Update(keyPress(k))\n\t}\n\n\t// Put cursor in the middle of the text\n\ttextarea.col = 4\n\n\ttextarea.InsertString(\"bar \")\n\n\tvalue := textarea.Value()\n\tif value != \"foo bar baz\" {\n\t\tt.Log(value)\n\t\tt.Fatal(\"Expected insert string to insert bar between foo and baz\")\n\t}\n}\n\nfunc TestCanHandleEmoji(t *testing.T) {\n\ttextarea := newTextArea()\n\tinput := \"🧋\"\n\n\tfor _, k := range []rune(input) {\n\t\ttextarea, _ = textarea.Update(keyPress(k))\n\t}\n\n\tvalue := textarea.Value()\n\tif value != input {\n\t\tt.Log(value)\n\t\tt.Fatal(\"Expected emoji to be inserted\")\n\t}\n\n\tinput = \"🧋🧋🧋\"\n\n\ttextarea.SetValue(input)\n\n\tvalue = textarea.Value()\n\tif value != input {\n\t\tt.Log(value)\n\t\tt.Fatal(\"Expected emoji to be inserted\")\n\t}\n\n\tif textarea.col != 3 {\n\t\tt.Log(textarea.col)\n\t\tt.Fatal(\"Expected cursor to be on the third character\")\n\t}\n\n\tif charOffset := textarea.LineInfo().CharOffset; charOffset != 6 {\n\t\tt.Log(charOffset)\n\t\tt.Fatal(\"Expected cursor to be on the sixth character\")\n\t}\n}\n\nfunc TestVerticalNavigationKeepsCursorHorizontalPosition(t *testing.T) {\n\ttextarea := newTextArea()\n\ttextarea.SetWidth(20)\n\n\ttextarea.SetValue(strings.Join([]string{\"你好你好\", \"Hello\"}, \"\\n\"))\n\n\ttextarea.row = 0\n\ttextarea.col = 2\n\n\t// 你好|你好\n\t// Hell|o\n\t// 1234|\n\n\t// Let's imagine our cursor is on the first line where the pipe is.\n\t// We press the down arrow to get to the next line.\n\t// The issue is that if we keep the cursor on the same column, the cursor will jump to after the `e`.\n\t//\n\t// 你好|你好\n\t// He|llo\n\t//\n\t// But this is wrong because visually we were at the 4th character due to\n\t// the first line containing double-width runes.\n\t// We want to keep the cursor on the same visual column.\n\t//\n\t// 你好|你好\n\t// Hell|o\n\t//\n\t// This test ensures that the cursor is kept on the same visual column by\n\t// ensuring that the column offset goes from 2 -> 4.\n\n\tlineInfo := textarea.LineInfo()\n\tif lineInfo.CharOffset != 4 || lineInfo.ColumnOffset != 2 {\n\t\tt.Log(lineInfo.CharOffset)\n\t\tt.Log(lineInfo.ColumnOffset)\n\t\tt.Fatal(\"Expected cursor to be on the fourth character because there are two double width runes on the first line.\")\n\t}\n\n\tdownMsg := tea.KeyPressMsg{Code: tea.KeyDown}\n\ttextarea, _ = textarea.Update(downMsg)\n\n\tlineInfo = textarea.LineInfo()\n\tif lineInfo.CharOffset != 4 || lineInfo.ColumnOffset != 4 {\n\t\tt.Log(lineInfo.CharOffset)\n\t\tt.Log(lineInfo.ColumnOffset)\n\t\tt.Fatal(\"Expected cursor to be on the fourth character because we came down from the first line.\")\n\t}\n}\n\nfunc TestVerticalNavigationShouldRememberPositionWhileTraversing(t *testing.T) {\n\ttextarea := newTextArea()\n\ttextarea.SetWidth(40)\n\n\t// Let's imagine we have a text area with the following content:\n\t//\n\t// Hello\n\t// World\n\t// This is a long line.\n\t//\n\t// If we are at the end of the last line and go up, we should be at the end\n\t// of the second line.\n\t// And, if we go up again we should be at the end of the first line.\n\t// But, if we go back down twice, we should be at the end of the last line\n\t// again and not the fifth (length of second line) character of the last line.\n\t//\n\t// In other words, we should remember the last horizontal position while\n\t// traversing vertically.\n\n\ttextarea.SetValue(strings.Join([]string{\"Hello\", \"World\", \"This is a long line.\"}, \"\\n\"))\n\n\t// We are at the end of the last line.\n\tif textarea.col != 20 || textarea.row != 2 {\n\t\tt.Log(textarea.col)\n\t\tt.Fatal(\"Expected cursor to be on the 20th character of the last line\")\n\t}\n\n\t// Let's go up.\n\tupMsg := tea.KeyPressMsg{Code: tea.KeyUp}\n\ttextarea, _ = textarea.Update(upMsg)\n\n\t// We should be at the end of the second line.\n\tif textarea.col != 5 || textarea.row != 1 {\n\t\tt.Log(textarea.col)\n\t\tt.Fatal(\"Expected cursor to be on the 5th character of the second line\")\n\t}\n\n\t// And, again.\n\ttextarea, _ = textarea.Update(upMsg)\n\n\t// We should be at the end of the first line.\n\tif textarea.col != 5 || textarea.row != 0 {\n\t\tt.Log(textarea.col)\n\t\tt.Fatal(\"Expected cursor to be on the 5th character of the first line\")\n\t}\n\n\t// Let's go down, twice.\n\tdownMsg := tea.KeyPressMsg{Code: tea.KeyDown}\n\ttextarea, _ = textarea.Update(downMsg)\n\ttextarea, _ = textarea.Update(downMsg)\n\n\t// We should be at the end of the last line.\n\tif textarea.col != 20 || textarea.row != 2 {\n\t\tt.Log(textarea.col)\n\t\tt.Fatal(\"Expected cursor to be on the 20th character of the last line\")\n\t}\n\n\t// Now, for correct behavior, if we move right or left, we should forget\n\t// (reset) the saved horizontal position. Since we assume the user wants to\n\t// keep the cursor where it is horizontally. This is how most text areas\n\t// work.\n\n\ttextarea, _ = textarea.Update(upMsg)\n\tleftMsg := tea.KeyPressMsg{Code: tea.KeyLeft}\n\ttextarea, _ = textarea.Update(leftMsg)\n\n\tif textarea.col != 4 || textarea.row != 1 {\n\t\tt.Log(textarea.col)\n\t\tt.Fatal(\"Expected cursor to be on the 5th character of the second line\")\n\t}\n\n\t// Going down now should keep us at the 4th column since we moved left and\n\t// reset the horizontal position saved state.\n\ttextarea, _ = textarea.Update(downMsg)\n\tif textarea.col != 4 || textarea.row != 2 {\n\t\tt.Log(textarea.col)\n\t\tt.Fatal(\"Expected cursor to be on the 4th character of the last line\")\n\t}\n}\n\nfunc TestView(t *testing.T) {\n\tt.Parallel()\n\n\ttype want struct {\n\t\tview      string\n\t\tcursorRow int\n\t\tcursorCol int\n\t}\n\n\ttests := []struct {\n\t\tname      string\n\t\tmodelFunc func(Model) Model\n\t\twant      want\n\t}{\n\t\t{\n\t\t\tname: \"placeholder\",\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t>   1 Hello, World!\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t`),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"single line\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.SetValue(\"the first line\")\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t>   1 the first line\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t`),\n\t\t\t\tcursorRow: 0,\n\t\t\t\tcursorCol: 14,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple lines\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.SetValue(\"the first line\\nthe second line\\nthe third line\")\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t>   1 the first line\n\t\t\t\t\t>   2 the second line\n\t\t\t\t\t>   3 the third line\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t`),\n\t\t\t\tcursorRow: 2,\n\t\t\t\tcursorCol: 14,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"single line without line numbers\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.SetValue(\"the first line\")\n\t\t\t\tm.ShowLineNumbers = false\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t> the first line\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t`),\n\t\t\t\tcursorRow: 0,\n\t\t\t\tcursorCol: 14,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"multipline lines without line numbers\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.SetValue(\"the first line\\nthe second line\\nthe third line\")\n\t\t\t\tm.ShowLineNumbers = false\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t> the first line\n\t\t\t\t\t> the second line\n\t\t\t\t\t> the third line\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t`),\n\t\t\t\tcursorRow: 2,\n\t\t\t\tcursorCol: 14,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"single line and custom end of buffer character\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.SetValue(\"the first line\")\n\t\t\t\tm.EndOfBufferCharacter = '*'\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t>   1 the first line\n\t\t\t\t\t> *\n\t\t\t\t\t> *\n\t\t\t\t\t> *\n\t\t\t\t\t> *\n\t\t\t\t\t> *\n\t\t\t\t`),\n\t\t\t\tcursorRow: 0,\n\t\t\t\tcursorCol: 14,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple lines and custom end of buffer character\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.SetValue(\"the first line\\nthe second line\\nthe third line\")\n\t\t\t\tm.EndOfBufferCharacter = '*'\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t>   1 the first line\n\t\t\t\t\t>   2 the second line\n\t\t\t\t\t>   3 the third line\n\t\t\t\t\t> *\n\t\t\t\t\t> *\n\t\t\t\t\t> *\n\t\t\t\t`),\n\t\t\t\tcursorRow: 2,\n\t\t\t\tcursorCol: 14,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"single line without line numbers and custom end of buffer character\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.SetValue(\"the first line\")\n\t\t\t\tm.ShowLineNumbers = false\n\t\t\t\tm.EndOfBufferCharacter = '*'\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t> the first line\n\t\t\t\t\t> *\n\t\t\t\t\t> *\n\t\t\t\t\t> *\n\t\t\t\t\t> *\n\t\t\t\t\t> *\n\t\t\t\t`),\n\t\t\t\tcursorRow: 0,\n\t\t\t\tcursorCol: 14,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple lines without line numbers and custom end of buffer character\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.SetValue(\"the first line\\nthe second line\\nthe third line\")\n\t\t\t\tm.ShowLineNumbers = false\n\t\t\t\tm.EndOfBufferCharacter = '*'\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t> the first line\n\t\t\t\t\t> the second line\n\t\t\t\t\t> the third line\n\t\t\t\t\t> *\n\t\t\t\t\t> *\n\t\t\t\t\t> *\n\t\t\t\t`),\n\t\t\t\tcursorRow: 2,\n\t\t\t\tcursorCol: 14,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"single line and custom prompt\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.SetValue(\"the first line\")\n\t\t\t\tm.Prompt = \"* \"\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t*   1 the first line\n\t\t\t\t\t*\n\t\t\t\t\t*\n\t\t\t\t\t*\n\t\t\t\t\t*\n\t\t\t\t\t*\n\t\t\t\t`),\n\t\t\t\tcursorRow: 0,\n\t\t\t\tcursorCol: 14,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple lines and custom prompt\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.SetValue(\"the first line\\nthe second line\\nthe third line\")\n\t\t\t\tm.Prompt = \"* \"\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t*   1 the first line\n\t\t\t\t\t*   2 the second line\n\t\t\t\t\t*   3 the third line\n\t\t\t\t\t*\n\t\t\t\t\t*\n\t\t\t\t\t*\n\t\t\t\t`),\n\t\t\t\tcursorRow: 2,\n\t\t\t\tcursorCol: 14,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"type single line\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tinput := \"foo\"\n\t\t\t\tm = sendString(m, input)\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t>   1 foo\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t`),\n\t\t\t\tcursorRow: 0,\n\t\t\t\tcursorCol: 3,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"type multiple lines\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tinput := \"foo\\nbar\\nbaz\"\n\t\t\t\tm = sendString(m, input)\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t>   1 foo\n\t\t\t\t\t>   2 bar\n\t\t\t\t\t>   3 baz\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t`),\n\t\t\t\tcursorRow: 2,\n\t\t\t\tcursorCol: 3,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"softwrap\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.ShowLineNumbers = false\n\t\t\t\tm.Prompt = \"\"\n\t\t\t\tm.SetWidth(5)\n\n\t\t\t\tinput := \"foo bar baz\"\n\t\t\t\tm = sendString(m, input)\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\tfoo\n\t\t\t\t\tbar\n\t\t\t\t\tbaz\n\n\n\n\t\t\t\t`),\n\t\t\t\tcursorRow: 2,\n\t\t\t\tcursorCol: 3,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"single line character limit\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.CharLimit = 7\n\n\t\t\t\tinput := \"foo bar baz\"\n\t\t\t\tm = sendString(m, input)\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t>   1 foo bar\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t`),\n\t\t\t\tcursorRow: 0,\n\t\t\t\tcursorCol: 7,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple lines character limit\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.CharLimit = 19\n\n\t\t\t\tinput := \"foo bar baz\\nfoo bar baz\"\n\t\t\t\tm = sendString(m, input)\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t>   1 foo bar baz\n\t\t\t\t\t>   2 foo bar\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t`),\n\t\t\t\tcursorRow: 1,\n\t\t\t\tcursorCol: 7,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"set width\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.SetWidth(10)\n\n\t\t\t\tinput := \"12\"\n\t\t\t\tm = sendString(m, input)\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t>   1 12\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t`),\n\t\t\t\tcursorRow: 0,\n\t\t\t\tcursorCol: 2,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"set width max length text minus one\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.SetWidth(10)\n\n\t\t\t\tinput := \"123\"\n\t\t\t\tm = sendString(m, input)\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t>   1 123\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t`),\n\t\t\t\tcursorRow: 0,\n\t\t\t\tcursorCol: 3,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"set width max length text\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.SetWidth(10)\n\n\t\t\t\tinput := \"1234\"\n\t\t\t\tm = sendString(m, input)\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t>   1 1234\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t`),\n\t\t\t\tcursorRow: 1,\n\t\t\t\tcursorCol: 0,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"set width max length text plus one\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.SetWidth(10)\n\n\t\t\t\tinput := \"12345\"\n\t\t\t\tm = sendString(m, input)\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t>   1 1234\n\t\t\t\t\t>     5\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t`),\n\t\t\t\tcursorRow: 1,\n\t\t\t\tcursorCol: 1,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"set width set max width minus one\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.MaxWidth = 10\n\t\t\t\tm.SetWidth(11)\n\n\t\t\t\tinput := \"123\"\n\t\t\t\tm = sendString(m, input)\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t>   1 123\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t`),\n\t\t\t\tcursorRow: 0,\n\t\t\t\tcursorCol: 3,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"set width set max width\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.MaxWidth = 10\n\t\t\t\tm.SetWidth(11)\n\n\t\t\t\tinput := \"1234\"\n\t\t\t\tm = sendString(m, input)\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t>   1 1234\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t`),\n\t\t\t\tcursorRow: 1,\n\t\t\t\tcursorCol: 0,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"set width set max width plus one\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.MaxWidth = 10\n\t\t\t\tm.SetWidth(11)\n\n\t\t\t\tinput := \"12345\"\n\t\t\t\tm = sendString(m, input)\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t>   1 1234\n\t\t\t\t\t>     5\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t`),\n\t\t\t\tcursorRow: 1,\n\t\t\t\tcursorCol: 1,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"set width min width minus one\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.SetWidth(6)\n\n\t\t\t\tinput := \"123\"\n\t\t\t\tm = sendString(m, input)\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t>   1 1\n\t\t\t\t\t>     2\n\t\t\t\t\t>     3\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t`),\n\t\t\t\tcursorRow: 3,\n\t\t\t\tcursorCol: 0,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"set width min width\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.SetWidth(7)\n\n\t\t\t\tinput := \"123\"\n\t\t\t\tm = sendString(m, input)\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t>   1 1\n\t\t\t\t\t>     2\n\t\t\t\t\t>     3\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t`),\n\t\t\t\tcursorRow: 3,\n\t\t\t\tcursorCol: 0,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"set width min width no line numbers\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.ShowLineNumbers = false\n\t\t\t\tm.SetWidth(0)\n\n\t\t\t\tinput := \"123\"\n\t\t\t\tm = sendString(m, input)\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t> 1\n\t\t\t\t\t> 2\n\t\t\t\t\t> 3\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t`),\n\t\t\t\tcursorRow: 3,\n\t\t\t\tcursorCol: 0,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"set width min width no line numbers no prompt\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.ShowLineNumbers = false\n\t\t\t\tm.Prompt = \"\"\n\t\t\t\tm.SetWidth(0)\n\n\t\t\t\tinput := \"123\"\n\t\t\t\tm = sendString(m, input)\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t1\n\t\t\t\t\t2\n\t\t\t\t\t3\n\n\n\n\t\t\t\t`),\n\t\t\t\tcursorRow: 3,\n\t\t\t\tcursorCol: 0,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"set width min width plus one\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.SetWidth(8)\n\n\t\t\t\tinput := \"123\"\n\t\t\t\tm = sendString(m, input)\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t>   1 12\n\t\t\t\t\t>     3\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t`),\n\t\t\t\tcursorRow: 1,\n\t\t\t\tcursorCol: 1,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"set width without line numbers max length text minus one\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.ShowLineNumbers = false\n\t\t\t\tm.SetWidth(6)\n\n\t\t\t\tinput := \"123\"\n\t\t\t\tm = sendString(m, input)\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t> 123\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t`),\n\t\t\t\tcursorRow: 0,\n\t\t\t\tcursorCol: 3,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"set width without line numbers max length text\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.ShowLineNumbers = false\n\t\t\t\tm.SetWidth(6)\n\n\t\t\t\tinput := \"1234\"\n\t\t\t\tm = sendString(m, input)\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t> 1234\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t`),\n\t\t\t\tcursorRow: 1,\n\t\t\t\tcursorCol: 0,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"set width without line numbers max length text plus one\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.ShowLineNumbers = false\n\t\t\t\tm.SetWidth(6)\n\n\t\t\t\tinput := \"12345\"\n\t\t\t\tm = sendString(m, input)\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t> 1234\n\t\t\t\t\t> 5\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t`),\n\t\t\t\tcursorRow: 1,\n\t\t\t\tcursorCol: 1,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"set width with style\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\ts := m.Styles()\n\t\t\t\ts.Focused.Base = lipgloss.NewStyle().Border(lipgloss.NormalBorder())\n\t\t\t\tm.SetStyles(s)\n\t\t\t\tm.Focus()\n\n\t\t\t\tm.SetWidth(12)\n\n\t\t\t\tinput := \"1\"\n\t\t\t\tm = sendString(m, input)\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t┌──────────┐\n\t\t\t\t\t│>   1 1   │\n\t\t\t\t\t│>         │\n\t\t\t\t\t│>         │\n\t\t\t\t\t│>         │\n\t\t\t\t\t│>         │\n\t\t\t\t\t│>         │\n\t\t\t\t\t└──────────┘\n\t\t\t\t`),\n\t\t\t\tcursorRow: 0,\n\t\t\t\tcursorCol: 1,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"set width with style max width minus one\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\ts := m.Styles()\n\t\t\t\ts.Focused.Base = lipgloss.NewStyle().Border(lipgloss.NormalBorder())\n\t\t\t\tm.SetStyles(s)\n\t\t\t\tm.Focus()\n\n\t\t\t\tm.SetWidth(12)\n\n\t\t\t\tinput := \"123\"\n\t\t\t\tm = sendString(m, input)\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t┌──────────┐\n\t\t\t\t\t│>   1 123 │\n\t\t\t\t\t│>         │\n\t\t\t\t\t│>         │\n\t\t\t\t\t│>         │\n\t\t\t\t\t│>         │\n\t\t\t\t\t│>         │\n\t\t\t\t\t└──────────┘\n\t\t\t\t`),\n\t\t\t\tcursorRow: 0,\n\t\t\t\tcursorCol: 3,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"set width with style max width\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\ts := m.Styles()\n\t\t\t\ts.Focused.Base = lipgloss.NewStyle().Border(lipgloss.NormalBorder())\n\t\t\t\tm.SetStyles(s)\n\t\t\t\tm.Focus()\n\n\t\t\t\tm.SetWidth(12)\n\n\t\t\t\tinput := \"1234\"\n\t\t\t\tm = sendString(m, input)\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t┌──────────┐\n\t\t\t\t\t│>   1 1234│\n\t\t\t\t\t│>         │\n\t\t\t\t\t│>         │\n\t\t\t\t\t│>         │\n\t\t\t\t\t│>         │\n\t\t\t\t\t│>         │\n\t\t\t\t\t└──────────┘\n\t\t\t\t`),\n\t\t\t\tcursorRow: 1,\n\t\t\t\tcursorCol: 0,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"set width with style max width plus one\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\ts := m.Styles()\n\t\t\t\ts.Focused.Base = lipgloss.NewStyle().Border(lipgloss.NormalBorder())\n\t\t\t\tm.SetStyles(s)\n\t\t\t\tm.Focus()\n\n\t\t\t\tm.SetWidth(12)\n\n\t\t\t\tinput := \"12345\"\n\t\t\t\tm = sendString(m, input)\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t┌──────────┐\n\t\t\t\t\t│>   1 1234│\n\t\t\t\t\t│>     5   │\n\t\t\t\t\t│>         │\n\t\t\t\t\t│>         │\n\t\t\t\t\t│>         │\n\t\t\t\t\t│>         │\n\t\t\t\t\t└──────────┘\n\t\t\t\t`),\n\t\t\t\tcursorRow: 1,\n\t\t\t\tcursorCol: 1,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"set width without line numbers with style\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\ts := m.Styles()\n\t\t\t\ts.Focused.Base = lipgloss.NewStyle().Border(lipgloss.NormalBorder())\n\t\t\t\tm.SetStyles(s)\n\t\t\t\tm.Focus()\n\n\t\t\t\tm.ShowLineNumbers = false\n\t\t\t\tm.SetWidth(12)\n\n\t\t\t\tinput := \"123456\"\n\t\t\t\tm = sendString(m, input)\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t┌──────────┐\n\t\t\t\t\t│> 123456  │\n\t\t\t\t\t│>         │\n\t\t\t\t\t│>         │\n\t\t\t\t\t│>         │\n\t\t\t\t\t│>         │\n\t\t\t\t\t│>         │\n\t\t\t\t\t└──────────┘\n\t\t\t\t`),\n\t\t\t\tcursorRow: 0,\n\t\t\t\tcursorCol: 6,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"set width without line numbers with style max width minus one\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\ts := m.Styles()\n\t\t\t\ts.Focused.Base = lipgloss.NewStyle().Border(lipgloss.NormalBorder())\n\t\t\t\tm.SetStyles(s)\n\t\t\t\tm.Focus()\n\n\t\t\t\tm.ShowLineNumbers = false\n\t\t\t\tm.SetWidth(12)\n\n\t\t\t\tinput := \"1234567\"\n\t\t\t\tm = sendString(m, input)\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t┌──────────┐\n\t\t\t\t\t│> 1234567 │\n\t\t\t\t\t│>         │\n\t\t\t\t\t│>         │\n\t\t\t\t\t│>         │\n\t\t\t\t\t│>         │\n\t\t\t\t\t│>         │\n\t\t\t\t\t└──────────┘\n\t\t\t\t`),\n\t\t\t\tcursorRow: 0,\n\t\t\t\tcursorCol: 7,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"set width without line numbers with style max width\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\ts := m.Styles()\n\t\t\t\ts.Focused.Base = lipgloss.NewStyle().Border(lipgloss.NormalBorder())\n\t\t\t\tm.SetStyles(s)\n\t\t\t\tm.Focus()\n\n\t\t\t\tm.ShowLineNumbers = false\n\t\t\t\tm.SetWidth(12)\n\n\t\t\t\tinput := \"12345678\"\n\t\t\t\tm = sendString(m, input)\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t┌──────────┐\n\t\t\t\t\t│> 12345678│\n\t\t\t\t\t│>         │\n\t\t\t\t\t│>         │\n\t\t\t\t\t│>         │\n\t\t\t\t\t│>         │\n\t\t\t\t\t│>         │\n\t\t\t\t\t└──────────┘\n\t\t\t\t`),\n\t\t\t\tcursorRow: 1,\n\t\t\t\tcursorCol: 0,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"set width without line numbers with style max width plus one\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\ts := m.Styles()\n\t\t\t\ts.Focused.Base = lipgloss.NewStyle().Border(lipgloss.NormalBorder())\n\t\t\t\tm.SetStyles(s)\n\t\t\t\tm.Focus()\n\n\t\t\t\tm.ShowLineNumbers = false\n\t\t\t\tm.SetWidth(12)\n\n\t\t\t\tinput := \"123456789\"\n\t\t\t\tm = sendString(m, input)\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t┌──────────┐\n\t\t\t\t\t│> 12345678│\n\t\t\t\t\t│> 9       │\n\t\t\t\t\t│>         │\n\t\t\t\t\t│>         │\n\t\t\t\t\t│>         │\n\t\t\t\t\t│>         │\n\t\t\t\t\t└──────────┘\n\t\t\t\t`),\n\t\t\t\tcursorRow: 1,\n\t\t\t\tcursorCol: 1,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"placeholder min width\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.SetWidth(0)\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t>   1 H\n\t\t\t\t\t>     e\n\t\t\t\t\t>     l\n\t\t\t\t\t>     l\n\t\t\t\t\t>     o\n\t\t\t\t\t>     ,\n\t\t\t\t`),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"placeholder single line\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.Placeholder = \"placeholder the first line\"\n\t\t\t\tm.ShowLineNumbers = false\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t> placeholder the first line\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t`),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"placeholder multiple lines\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.Placeholder = \"placeholder the first line\\nplaceholder the second line\\nplaceholder the third line\"\n\t\t\t\tm.ShowLineNumbers = false\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t> placeholder the first line\n\t\t\t\t\t> placeholder the second line\n\t\t\t\t\t> placeholder the third line\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t`),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"placeholder single line with line numbers\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.Placeholder = \"placeholder the first line\"\n\t\t\t\tm.ShowLineNumbers = true\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t>   1 placeholder the first line\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t`),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"placeholder multiple lines with line numbers\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.Placeholder = \"placeholder the first line\\nplaceholder the second line\\nplaceholder the third line\"\n\t\t\t\tm.ShowLineNumbers = true\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t>   1 placeholder the first line\n\t\t\t\t\t>     placeholder the second line\n\t\t\t\t\t>     placeholder the third line\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t`),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"placeholder single line with end of buffer character\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.Placeholder = \"placeholder the first line\"\n\t\t\t\tm.ShowLineNumbers = false\n\t\t\t\tm.EndOfBufferCharacter = '*'\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t> placeholder the first line\n\t\t\t\t\t> *\n\t\t\t\t\t> *\n\t\t\t\t\t> *\n\t\t\t\t\t> *\n\t\t\t\t\t> *\n\t\t\t\t`),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"placeholder multiple lines with with end of buffer character\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.Placeholder = \"placeholder the first line\\nplaceholder the second line\\nplaceholder the third line\"\n\t\t\t\tm.ShowLineNumbers = false\n\t\t\t\tm.EndOfBufferCharacter = '*'\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t> placeholder the first line\n\t\t\t\t\t> placeholder the second line\n\t\t\t\t\t> placeholder the third line\n\t\t\t\t\t> *\n\t\t\t\t\t> *\n\t\t\t\t\t> *\n\t\t\t\t`),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"placeholder single line with line numbers and end of buffer character\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.Placeholder = \"placeholder the first line\"\n\t\t\t\tm.ShowLineNumbers = true\n\t\t\t\tm.EndOfBufferCharacter = '*'\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t>   1 placeholder the first line\n\t\t\t\t\t> *\n\t\t\t\t\t> *\n\t\t\t\t\t> *\n\t\t\t\t\t> *\n\t\t\t\t\t> *\n\t\t\t\t`),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"placeholder multiple lines with line numbers and end of buffer character\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.Placeholder = \"placeholder the first line\\nplaceholder the second line\\nplaceholder the third line\"\n\t\t\t\tm.ShowLineNumbers = true\n\t\t\t\tm.EndOfBufferCharacter = '*'\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t>   1 placeholder the first line\n\t\t\t\t\t>     placeholder the second line\n\t\t\t\t\t>     placeholder the third line\n\t\t\t\t\t> *\n\t\t\t\t\t> *\n\t\t\t\t\t> *\n\t\t\t\t`),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"placeholder single line that is longer than max width\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.Placeholder = \"placeholder the first line that is longer than the max width\"\n\t\t\t\tm.SetWidth(40)\n\t\t\t\tm.ShowLineNumbers = false\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t> placeholder the first line that is\n\t\t\t\t\t> longer than the max width\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t`),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"placeholder multiple lines that are longer than max width\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.Placeholder = \"placeholder the first line that is longer than the max width\\nplaceholder the second line that is longer than the max width\"\n\t\t\t\tm.ShowLineNumbers = false\n\t\t\t\tm.SetWidth(40)\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t> placeholder the first line that is\n\t\t\t\t\t> longer than the max width\n\t\t\t\t\t> placeholder the second line that is\n\t\t\t\t\t> longer than the max width\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t`),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"placeholder single line that is longer than max width with line numbers\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.Placeholder = \"placeholder the first line that is longer than the max width\"\n\t\t\t\tm.ShowLineNumbers = true\n\t\t\t\tm.SetWidth(40)\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t>   1 placeholder the first line that is\n\t\t\t\t\t>     longer than the max width\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t`),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"placeholder multiple lines that are longer than max width with line numbers\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.Placeholder = \"placeholder the first line that is longer than the max width\\nplaceholder the second line that is longer than the max width\"\n\t\t\t\tm.ShowLineNumbers = true\n\t\t\t\tm.SetWidth(40)\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t>   1 placeholder the first line that is\n\t\t\t\t\t>     longer than the max width\n\t\t\t\t\t>     placeholder the second line that\n\t\t\t\t\t>     is longer than the max width\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t`),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"placeholder single line that is longer than max width at limit\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.Placeholder = \"123456789012345678\"\n\t\t\t\tm.ShowLineNumbers = false\n\t\t\t\tm.SetWidth(20)\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t> 123456789012345678\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t`),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"placeholder single line that is longer than max width at limit plus one\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.Placeholder = \"1234567890123456789\"\n\t\t\t\tm.ShowLineNumbers = false\n\t\t\t\tm.SetWidth(20)\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t> 123456789012345678\n\t\t\t\t\t> 9\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t`),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"placeholder single line that is longer than max width with line numbers at limit\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.Placeholder = \"12345678901234\"\n\t\t\t\tm.ShowLineNumbers = true\n\t\t\t\tm.SetWidth(20)\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t>   1 12345678901234\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t`),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"placeholder single line that is longer than max width with line numbers at limit plus one\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.Placeholder = \"123456789012345\"\n\t\t\t\tm.ShowLineNumbers = true\n\t\t\t\tm.SetWidth(20)\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t>   1 12345678901234\n\t\t\t\t\t>     5\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t`),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"placeholder multiple lines that are longer than max width at limit\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.Placeholder = \"123456789012345678\\n123456789012345678\"\n\t\t\t\tm.ShowLineNumbers = false\n\t\t\t\tm.SetWidth(20)\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t> 123456789012345678\n\t\t\t\t\t> 123456789012345678\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t`),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"placeholder multiple lines that are longer than max width at limit plus one\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.Placeholder = \"1234567890123456789\\n1234567890123456789\"\n\t\t\t\tm.ShowLineNumbers = false\n\t\t\t\tm.SetWidth(20)\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t> 123456789012345678\n\t\t\t\t\t> 9\n\t\t\t\t\t> 123456789012345678\n\t\t\t\t\t> 9\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t`),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"placeholder multiple lines that are longer than max width with line numbers at limit\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.Placeholder = \"12345678901234\\n12345678901234\"\n\t\t\t\tm.ShowLineNumbers = true\n\t\t\t\tm.SetWidth(20)\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t>   1 12345678901234\n\t\t\t\t\t>     12345678901234\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t`),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"placeholder multiple lines that are longer than max width with line numbers at limit plus one\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.Placeholder = \"123456789012345\\n123456789012345\"\n\t\t\t\tm.ShowLineNumbers = true\n\t\t\t\tm.SetWidth(20)\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t>   1 12345678901234\n\t\t\t\t\t>     5\n\t\t\t\t\t>     12345678901234\n\t\t\t\t\t>     5\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t`),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"placeholder chinese character\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.Placeholder = \"输入消息...\"\n\t\t\t\tm.ShowLineNumbers = true\n\t\t\t\tm.SetWidth(20)\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t>   1 输入消息...\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\t\t\t\t\t>\n\n\t\t\t\t`),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"page up moves to beginning when near top\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.ShowLineNumbers = true\n\t\t\t\tm.SetHeight(4)\n\t\t\t\tm.SetWidth(20)\n\n\t\t\t\tlines := make([]string, 10)\n\t\t\t\tfor i := range 10 {\n\t\t\t\t\tlines[i] = fmt.Sprintf(\"Line %d\", i+1)\n\t\t\t\t}\n\t\t\t\tm.SetValue(strings.Join(lines, \"\\n\"))\n\t\t\t\tm.viewport.SetContent(m.view()) // force setting of viewport content.\n\n\t\t\t\tm.row = 3\n\t\t\t\tm.col = 0\n\t\t\t\tm.viewport.SetYOffset(0)\n\t\t\t\tm.PageUp()\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t>   1 Line 1\n\t\t\t\t\t>   2 Line 2\n\t\t\t\t\t>   3 Line 3\n\t\t\t\t\t>   4 Line 4\n\t\t\t\t`),\n\t\t\t\tcursorRow: 0,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"page up snaps to first visible line when not on it\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.ShowLineNumbers = true\n\t\t\t\tm.SetHeight(4)\n\t\t\t\tm.SetWidth(20)\n\n\t\t\t\tlines := make([]string, 10)\n\t\t\t\tfor i := range 10 {\n\t\t\t\t\tlines[i] = fmt.Sprintf(\"Line %d\", i+1)\n\t\t\t\t}\n\t\t\t\tm.SetValue(strings.Join(lines, \"\\n\"))\n\t\t\t\tm.viewport.SetContent(m.view()) // force setting of viewport content.\n\n\t\t\t\tm.row = 5\n\t\t\t\tm.col = 0\n\t\t\t\tm.viewport.SetYOffset(3)\n\t\t\t\tm.PageUp()\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t>   4 Line 4\n\t\t\t\t\t>   5 Line 5\n\t\t\t\t\t>   6 Line 6\n\t\t\t\t\t>   7 Line 7\n\t\t\t\t`),\n\t\t\t\tcursorRow: 3,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"page up moves up by full page when on first visible line\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.ShowLineNumbers = true\n\t\t\t\tm.SetHeight(3)\n\t\t\t\tm.SetWidth(20)\n\n\t\t\t\tlines := make([]string, 10)\n\t\t\t\tfor i := range 10 {\n\t\t\t\t\tlines[i] = fmt.Sprintf(\"Line %d\", i+1)\n\t\t\t\t}\n\t\t\t\tm.SetValue(strings.Join(lines, \"\\n\"))\n\t\t\t\tm.viewport.SetContent(m.view()) // force setting of viewport content.\n\n\t\t\t\tm.row = 5\n\t\t\t\tm.col = 0\n\t\t\t\tm.viewport.SetYOffset(5)\n\t\t\t\tm.PageUp()\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t>   3 Line 3\n\t\t\t\t\t>   4 Line 4\n\t\t\t\t\t>   5 Line 5\n\t\t\t\t`),\n\t\t\t\tcursorRow: 2,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"page down moves to end when near bottom\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.SetHeight(3)\n\t\t\t\tm.SetWidth(20)\n\n\t\t\t\tlines := make([]string, 10)\n\t\t\t\tfor i := range 10 {\n\t\t\t\t\tlines[i] = fmt.Sprintf(\"Line %d\", i+1)\n\t\t\t\t}\n\t\t\t\tm.SetValue(strings.Join(lines, \"\\n\"))\n\t\t\t\tm.viewport.SetContent(m.view()) // force setting of viewport content.\n\n\t\t\t\tm.row = 8\n\t\t\t\tm.col = 0\n\t\t\t\tm.viewport.SetYOffset(7)\n\t\t\t\tm.PageDown()\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t>   8 Line 8\n\t\t\t\t\t>   9 Line 9\n\t\t\t\t\t>  10 Line 10\n\t\t\t\t`),\n\t\t\t\tcursorRow: 9,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"page down snaps to last visible line when not on it\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.SetHeight(3)\n\t\t\t\tm.SetWidth(20)\n\n\t\t\t\tlines := make([]string, 10)\n\t\t\t\tfor i := range 10 {\n\t\t\t\t\tlines[i] = fmt.Sprintf(\"Line %d\", i+1)\n\t\t\t\t}\n\t\t\t\tm.SetValue(strings.Join(lines, \"\\n\"))\n\t\t\t\tm.viewport.SetContent(m.view()) // force setting of viewport content.\n\n\t\t\t\tm.row = 3\n\t\t\t\tm.col = 0\n\t\t\t\tm.viewport.SetYOffset(3)\n\t\t\t\tm.PageDown()\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t>   4 Line 4\n\t\t\t\t\t>   5 Line 5\n\t\t\t\t\t>   6 Line 6\n\t\t\t\t`),\n\t\t\t\tcursorRow: 5,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"page down moves down by full page when on last visible line\",\n\t\t\tmodelFunc: func(m Model) Model {\n\t\t\t\tm.SetHeight(3)\n\t\t\t\tm.SetWidth(20)\n\n\t\t\t\tlines := make([]string, 10)\n\t\t\t\tfor i := range 10 {\n\t\t\t\t\tlines[i] = fmt.Sprintf(\"Line %d\", i+1)\n\t\t\t\t}\n\t\t\t\tm.SetValue(strings.Join(lines, \"\\n\"))\n\t\t\t\tm.viewport.SetContent(m.view()) // force setting of viewport content.\n\n\t\t\t\tm.row = 4\n\t\t\t\tm.col = 0\n\t\t\t\tm.viewport.SetYOffset(2)\n\t\t\t\tm.PageDown()\n\n\t\t\t\treturn m\n\t\t\t},\n\t\t\twant: want{\n\t\t\t\tview: heredoc.Doc(`\n\t\t\t\t\t>   6 Line 6\n\t\t\t\t\t>   7 Line 7\n\t\t\t\t\t>   8 Line 8\n\t\t\t\t`),\n\t\t\t\tcursorRow: 7,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttextarea := newTextArea()\n\n\t\t\tif tt.modelFunc != nil {\n\t\t\t\ttextarea = tt.modelFunc(textarea)\n\t\t\t}\n\n\t\t\tview := stripString(textarea.View())\n\t\t\twantView := stripString(tt.want.view)\n\n\t\t\tif view != wantView {\n\t\t\t\tt.Fatalf(\"Want:\\n%v\\nGot:\\n%v\\n\", wantView, view)\n\t\t\t}\n\n\t\t\tcursorRow := textarea.cursorLineNumber()\n\t\t\tcursorCol := textarea.LineInfo().ColumnOffset\n\t\t\tif tt.want.cursorRow != cursorRow || tt.want.cursorCol != cursorCol {\n\t\t\t\tformat := \"Want cursor at row: %v, col: %v Got: row: %v col: %v\\n\"\n\t\t\t\tt.Fatalf(format, tt.want.cursorRow, tt.want.cursorCol, cursorRow, cursorCol)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestWord(t *testing.T) {\n\ttextarea := newTextArea()\n\n\ttextarea.SetHeight(3)\n\ttextarea.SetWidth(20)\n\ttextarea.CharLimit = 500\n\n\ttextarea, _ = textarea.Update(nil)\n\n\tt.Run(\"regular input\", func(t *testing.T) {\n\t\tinput := \"Word1 Word2 Word3 Word4\"\n\t\tfor _, k := range input {\n\t\t\ttextarea, _ = textarea.Update(keyPress(k))\n\t\t\ttextarea.View()\n\t\t}\n\n\t\texpect := \"Word4\"\n\t\tif word := textarea.Word(); word != expect {\n\t\t\tt.Fatalf(\"Expected last word to be '%s', got '%s'\", expect, word)\n\t\t}\n\t})\n\n\tt.Run(\"navigate\", func(t *testing.T) {\n\t\tfor _, k := range []tea.KeyPressMsg{\n\t\t\t{Code: tea.KeyLeft, Mod: tea.ModAlt, Text: \"alt+left\"},\n\t\t\t{Code: tea.KeyLeft, Mod: tea.ModAlt, Text: \"alt+left\"},\n\t\t\t{Code: tea.KeyRight, Text: \"right\"},\n\t\t} {\n\t\t\ttextarea, _ = textarea.Update(k)\n\t\t\ttextarea.View()\n\t\t}\n\n\t\texpect := \"Word3\"\n\t\tif word := textarea.Word(); word != expect {\n\t\t\tt.Fatalf(\"Expected last word to be '%s', got '%s'\", expect, word)\n\t\t}\n\t})\n\n\tt.Run(\"delete\", func(t *testing.T) {\n\t\tfor _, k := range []tea.KeyPressMsg{\n\t\t\t{Code: tea.KeyEnd, Text: \"end\"},\n\t\t\t{Code: tea.KeyBackspace, Mod: tea.ModAlt, Text: \"alt+backspace\"},\n\t\t\t{Code: tea.KeyBackspace, Mod: tea.ModAlt, Text: \"alt+backspace\"},\n\t\t\t{Code: tea.KeyBackspace, Text: \"backspace\"},\n\t\t} {\n\t\t\ttextarea, _ = textarea.Update(k)\n\t\t\ttextarea.View()\n\t\t}\n\n\t\texpect := \"Word2\"\n\t\tif word := textarea.Word(); word != expect {\n\t\t\tt.Fatalf(\"Expected last word to be '%s', got '%s'\", expect, word)\n\t\t}\n\t})\n}\n\nfunc newTextArea() Model {\n\ttextarea := New()\n\n\ttextarea.Prompt = \"> \"\n\ttextarea.Placeholder = \"Hello, World!\"\n\n\ttextarea.Focus()\n\n\ttextarea, _ = textarea.Update(nil)\n\n\treturn textarea\n}\n\nfunc keyPress(key rune) tea.Msg {\n\treturn tea.KeyPressMsg{Code: key, Text: string(key)}\n}\n\nfunc sendString(m Model, str string) Model {\n\tfor _, k := range []rune(str) {\n\t\tm, _ = m.Update(keyPress(k))\n\t}\n\n\treturn m\n}\n\nfunc stripString(str string) string {\n\ts := ansi.Strip(str)\n\tss := strings.Split(s, \"\\n\")\n\n\tvar lines []string\n\tfor _, l := range ss {\n\t\ttrim := strings.TrimRightFunc(l, unicode.IsSpace)\n\t\tif trim != \"\" {\n\t\t\tlines = append(lines, trim)\n\t\t}\n\t}\n\n\treturn strings.Join(lines, \"\\n\")\n}\n"
  },
  {
    "path": "textinput/styles.go",
    "content": "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// DefaultStyles returns the default styles for focused and blurred states for\n// the textarea.\nfunc DefaultStyles(isDark bool) Styles {\n\tlightDark := lipgloss.LightDark(isDark)\n\n\tvar s Styles\n\ts.Focused = StyleState{\n\t\tPlaceholder: lipgloss.NewStyle().Foreground(lipgloss.Color(\"240\")),\n\t\tSuggestion:  lipgloss.NewStyle().Foreground(lipgloss.Color(\"240\")),\n\t\tPrompt:      lipgloss.NewStyle().Foreground(lipgloss.Color(\"7\")),\n\t\tText:        lipgloss.NewStyle(),\n\t}\n\ts.Blurred = StyleState{\n\t\tPlaceholder: lipgloss.NewStyle().Foreground(lipgloss.Color(\"240\")),\n\t\tSuggestion:  lipgloss.NewStyle().Foreground(lipgloss.Color(\"240\")),\n\t\tPrompt:      lipgloss.NewStyle().Foreground(lipgloss.Color(\"7\")),\n\t\tText:        lipgloss.NewStyle().Foreground(lightDark(lipgloss.Color(\"245\"), lipgloss.Color(\"7\"))),\n\t}\n\ts.Cursor = CursorStyle{\n\t\tColor: lipgloss.Color(\"7\"),\n\t\tShape: tea.CursorBlock,\n\t\tBlink: true,\n\t}\n\treturn s\n}\n\n// DefaultLightStyles returns the default styles for a light background.\nfunc DefaultLightStyles() Styles {\n\treturn DefaultStyles(false)\n}\n\n// DefaultDarkStyles returns the default styles for a dark background.\nfunc DefaultDarkStyles() Styles {\n\treturn DefaultStyles(true)\n}\n\n// Styles are the styles for the textarea, separated into focused and blurred\n// states. The appropriate styles will be chosen based on the focus state of\n// the textarea.\ntype Styles struct {\n\tFocused StyleState\n\tBlurred StyleState\n\tCursor  CursorStyle\n}\n\n// StyleState that will be applied to the text area.\n//\n// StyleState can be applied to focused and unfocused states to change the styles\n// depending on the focus state.\n//\n// For an introduction to styling with Lip Gloss see:\n// https://github.com/charmbracelet/lipgloss\ntype StyleState struct {\n\tText        lipgloss.Style\n\tPlaceholder lipgloss.Style\n\tSuggestion  lipgloss.Style\n\tPrompt      lipgloss.Style\n}\n\n// CursorStyle is the style for real and virtual cursors.\ntype CursorStyle struct {\n\t// Style styles the cursor block.\n\t//\n\t// For real cursors, the foreground color set here will be used as the\n\t// cursor color.\n\tColor color.Color\n\n\t// Shape is the cursor shape. The following shapes are available:\n\t//\n\t// - tea.CursorBlock\n\t// - tea.CursorUnderline\n\t// - tea.CursorBar\n\t//\n\t// This is only used for real cursors.\n\tShape tea.CursorShape\n\n\t// CursorBlink determines whether or not the cursor should blink.\n\tBlink bool\n\n\t// BlinkSpeed is the speed at which the virtual cursor blinks. This has no\n\t// effect on real cursors as well as no effect if the cursor is set not to\n\t// [CursorBlink].\n\t//\n\t// By default, the blink speed is set to about 500ms.\n\tBlinkSpeed time.Duration\n}\n"
  },
  {
    "path": "textinput/textinput.go",
    "content": "// Package textinput provides a text input component for Bubble Tea\n// applications.\npackage textinput\n\nimport (\n\t\"reflect\"\n\t\"slices\"\n\t\"strings\"\n\t\"unicode\"\n\n\t\"charm.land/bubbles/v2/cursor\"\n\t\"charm.land/bubbles/v2/internal/runeutil\"\n\t\"charm.land/bubbles/v2/key\"\n\ttea \"charm.land/bubbletea/v2\"\n\t\"charm.land/lipgloss/v2\"\n\t\"github.com/atotto/clipboard\"\n\trw \"github.com/mattn/go-runewidth\"\n\t\"github.com/rivo/uniseg\"\n)\n\n// Internal messages for clipboard operations.\ntype (\n\tpasteMsg    string\n\tpasteErrMsg struct{ error }\n)\n\n// EchoMode sets the input behavior of the text input field.\ntype EchoMode int\n\nconst (\n\t// EchoNormal displays text as is. This is the default behavior.\n\tEchoNormal EchoMode = iota\n\n\t// EchoPassword displays the EchoCharacter mask instead of actual\n\t// characters. This is commonly used for password fields.\n\tEchoPassword\n\n\t// EchoNone displays nothing as characters are entered. This is commonly\n\t// seen for password fields on the command line.\n\tEchoNone\n)\n\n// ValidateFunc is a function that returns an error if the input is invalid.\ntype ValidateFunc func(string) error\n\n// KeyMap is the key bindings for different actions within the textinput.\ntype KeyMap struct {\n\tCharacterForward        key.Binding\n\tCharacterBackward       key.Binding\n\tWordForward             key.Binding\n\tWordBackward            key.Binding\n\tDeleteWordBackward      key.Binding\n\tDeleteWordForward       key.Binding\n\tDeleteAfterCursor       key.Binding\n\tDeleteBeforeCursor      key.Binding\n\tDeleteCharacterBackward key.Binding\n\tDeleteCharacterForward  key.Binding\n\tLineStart               key.Binding\n\tLineEnd                 key.Binding\n\tPaste                   key.Binding\n\tAcceptSuggestion        key.Binding\n\tNextSuggestion          key.Binding\n\tPrevSuggestion          key.Binding\n}\n\n// DefaultKeyMap is the default set of key bindings for navigating and acting\n// upon the textinput.\nfunc DefaultKeyMap() KeyMap {\n\treturn KeyMap{\n\t\tCharacterForward:        key.NewBinding(key.WithKeys(\"right\", \"ctrl+f\")),\n\t\tCharacterBackward:       key.NewBinding(key.WithKeys(\"left\", \"ctrl+b\")),\n\t\tWordForward:             key.NewBinding(key.WithKeys(\"alt+right\", \"ctrl+right\", \"alt+f\")),\n\t\tWordBackward:            key.NewBinding(key.WithKeys(\"alt+left\", \"ctrl+left\", \"alt+b\")),\n\t\tDeleteWordBackward:      key.NewBinding(key.WithKeys(\"alt+backspace\", \"ctrl+w\")),\n\t\tDeleteWordForward:       key.NewBinding(key.WithKeys(\"alt+delete\", \"alt+d\")),\n\t\tDeleteAfterCursor:       key.NewBinding(key.WithKeys(\"ctrl+k\")),\n\t\tDeleteBeforeCursor:      key.NewBinding(key.WithKeys(\"ctrl+u\")),\n\t\tDeleteCharacterBackward: key.NewBinding(key.WithKeys(\"backspace\", \"ctrl+h\")),\n\t\tDeleteCharacterForward:  key.NewBinding(key.WithKeys(\"delete\", \"ctrl+d\")),\n\t\tLineStart:               key.NewBinding(key.WithKeys(\"home\", \"ctrl+a\")),\n\t\tLineEnd:                 key.NewBinding(key.WithKeys(\"end\", \"ctrl+e\")),\n\t\tPaste:                   key.NewBinding(key.WithKeys(\"ctrl+v\")),\n\t\tAcceptSuggestion:        key.NewBinding(key.WithKeys(\"tab\")),\n\t\tNextSuggestion:          key.NewBinding(key.WithKeys(\"down\", \"ctrl+n\")),\n\t\tPrevSuggestion:          key.NewBinding(key.WithKeys(\"up\", \"ctrl+p\")),\n\t}\n}\n\n// Model is the Bubble Tea model for this text input element.\ntype Model struct {\n\tErr error\n\n\t// General settings.\n\tPrompt        string\n\tPlaceholder   string\n\tEchoMode      EchoMode\n\tEchoCharacter rune\n\n\t// useVirtualCursor determines whether or not to use the virtual cursor. If\n\t// set to false, use [Model.Cursor] to return a real cursor for rendering.\n\tuseVirtualCursor bool\n\n\t// Virtual cursor manager.\n\tvirtualCursor cursor.Model\n\n\t// CharLimit is the maximum amount of characters this input element will\n\t// accept. If 0 or less, there's no limit.\n\tCharLimit int\n\n\t// Styling. FocusedStyle and BlurredStyle are used to style the textarea in\n\t// focused and blurred states.\n\tstyles Styles\n\n\t// Width is the maximum number of characters that can be displayed at once.\n\t// It essentially treats the text field like a horizontally scrolling\n\t// viewport. If 0 or less this setting is ignored.\n\twidth int\n\n\t// KeyMap encodes the keybindings recognized by the widget.\n\tKeyMap KeyMap\n\n\t// Underlying text value.\n\tvalue []rune\n\n\t// focus indicates whether user input focus should be on this input\n\t// component. When false, ignore keyboard input and hide the cursor.\n\tfocus bool\n\n\t// Cursor position.\n\tpos int\n\n\t// Used to emulate a viewport when width is set and the content is\n\t// overflowing.\n\toffset      int\n\toffsetRight int\n\n\t// Validate is a function that checks whether or not the text within the\n\t// input is valid. If it is not valid, the `Err` field will be set to the\n\t// error returned by the function. If the function is not defined, all\n\t// input is considered valid.\n\tValidate ValidateFunc\n\n\t// rune sanitizer for input.\n\trsan runeutil.Sanitizer\n\n\t// Should the input suggest to complete\n\tShowSuggestions bool\n\n\t// suggestions is a list of suggestions that may be used to complete the\n\t// input.\n\tsuggestions            [][]rune\n\tmatchedSuggestions     [][]rune\n\tcurrentSuggestionIndex int\n}\n\n// New creates a new model with default settings.\nfunc New() Model {\n\tm := Model{\n\t\tPrompt:           \"> \",\n\t\tEchoCharacter:    '*',\n\t\tCharLimit:        0,\n\t\tstyles:           DefaultDarkStyles(),\n\t\tShowSuggestions:  false,\n\t\tuseVirtualCursor: true,\n\t\tvirtualCursor:    cursor.New(),\n\t\tKeyMap:           DefaultKeyMap(),\n\t\tsuggestions:      [][]rune{},\n\t\tvalue:            nil,\n\t\tfocus:            false,\n\t\tpos:              0,\n\t}\n\tm.updateVirtualCursorStyle()\n\treturn m\n}\n\n// VirtualCursor returns whether the model is using a virtual cursor.\nfunc (m Model) VirtualCursor() bool {\n\treturn m.useVirtualCursor\n}\n\n// SetVirtualCursor sets whether the model should use a virtual cursor. If\n// disabled, use [Model.Cursor] to return a real cursor for rendering.\nfunc (m *Model) SetVirtualCursor(v bool) {\n\tm.useVirtualCursor = v\n\tm.updateVirtualCursorStyle()\n}\n\n// Styles returns the current set of styles.\nfunc (m Model) Styles() Styles {\n\treturn m.styles\n}\n\n// SetStyles sets the styles for the text input.\nfunc (m *Model) SetStyles(s Styles) {\n\tm.styles = s\n\tm.updateVirtualCursorStyle()\n}\n\n// Width returns the width of the text input.\nfunc (m Model) Width() int {\n\treturn m.width\n}\n\n// SetWidth sets the width of the text input.\nfunc (m *Model) SetWidth(w int) {\n\tm.width = w\n}\n\n// SetValue sets the value of the text input.\nfunc (m *Model) SetValue(s string) {\n\t// Clean up any special characters in the input provided by the\n\t// caller. This avoids bugs due to e.g. tab characters and whatnot.\n\trunes := m.san().Sanitize([]rune(s))\n\terr := m.validate(runes)\n\tm.setValueInternal(runes, err)\n}\n\nfunc (m *Model) setValueInternal(runes []rune, err error) {\n\tm.Err = err\n\n\tempty := len(m.value) == 0\n\n\tif m.CharLimit > 0 && len(runes) > m.CharLimit {\n\t\tm.value = runes[:m.CharLimit]\n\t} else {\n\t\tm.value = runes\n\t}\n\tif (m.pos == 0 && empty) || m.pos > len(m.value) {\n\t\tm.SetCursor(len(m.value))\n\t}\n\tm.handleOverflow()\n}\n\n// Value returns the value of the text input.\nfunc (m Model) Value() string {\n\treturn string(m.value)\n}\n\n// Position returns the cursor position.\nfunc (m Model) Position() int {\n\treturn m.pos\n}\n\n// SetCursor moves the cursor to the given position. If the position is\n// out of bounds the cursor will be moved to the start or end accordingly.\nfunc (m *Model) SetCursor(pos int) {\n\tm.pos = clamp(pos, 0, len(m.value))\n\tm.handleOverflow()\n}\n\n// CursorStart moves the cursor to the start of the input field.\nfunc (m *Model) CursorStart() {\n\tm.SetCursor(0)\n}\n\n// CursorEnd moves the cursor to the end of the input field.\nfunc (m *Model) CursorEnd() {\n\tm.SetCursor(len(m.value))\n}\n\n// Focused returns the focus state on the model.\nfunc (m Model) Focused() bool {\n\treturn m.focus\n}\n\n// Focus sets the focus state on the model. When the model is in focus it can\n// receive keyboard input and the cursor will be shown.\nfunc (m *Model) Focus() tea.Cmd {\n\tm.focus = true\n\treturn m.virtualCursor.Focus()\n}\n\n// Blur removes the focus state on the model.  When the model is blurred it can\n// not receive keyboard input and the cursor will be hidden.\nfunc (m *Model) Blur() {\n\tm.focus = false\n\tm.virtualCursor.Blur()\n}\n\n// Reset sets the input to its default state with no input.\nfunc (m *Model) Reset() {\n\tm.value = nil\n\tm.SetCursor(0)\n}\n\n// SetSuggestions sets the suggestions for the input.\nfunc (m *Model) SetSuggestions(suggestions []string) {\n\tm.suggestions = make([][]rune, len(suggestions))\n\tfor i, s := range suggestions {\n\t\tm.suggestions[i] = []rune(s)\n\t}\n\n\tm.updateSuggestions()\n}\n\n// rsan initializes or retrieves the rune sanitizer.\nfunc (m *Model) san() runeutil.Sanitizer {\n\tif m.rsan == nil {\n\t\t// Textinput has all its input on a single line so collapse\n\t\t// newlines/tabs to single spaces.\n\t\tm.rsan = runeutil.NewSanitizer(\n\t\t\truneutil.ReplaceTabs(\" \"), runeutil.ReplaceNewlines(\" \"))\n\t}\n\treturn m.rsan\n}\n\nfunc (m *Model) insertRunesFromUserInput(v []rune) {\n\t// Clean up any special characters in the input provided by the\n\t// clipboard. This avoids bugs due to e.g. tab characters and\n\t// whatnot.\n\tpaste := m.san().Sanitize(v)\n\n\tvar availSpace int\n\tif m.CharLimit > 0 {\n\t\tavailSpace = m.CharLimit - len(m.value)\n\n\t\t// If the char limit's been reached, cancel.\n\t\tif availSpace <= 0 {\n\t\t\treturn\n\t\t}\n\n\t\t// If there's not enough space to paste the whole thing cut the pasted\n\t\t// runes down so they'll fit.\n\t\tif availSpace < len(paste) {\n\t\t\tpaste = paste[:availSpace]\n\t\t}\n\t}\n\n\t// Stuff before and after the cursor\n\thead := m.value[:m.pos]\n\ttailSrc := m.value[m.pos:]\n\ttail := make([]rune, len(tailSrc))\n\tcopy(tail, tailSrc)\n\n\t// Insert pasted runes\n\tfor _, r := range paste {\n\t\thead = append(head, r)\n\t\tm.pos++\n\t\tif m.CharLimit > 0 {\n\t\t\tavailSpace--\n\t\t\tif availSpace <= 0 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\t// Put it all back together\n\tvalue := append(head, tail...)\n\tinputErr := m.validate(value)\n\tm.setValueInternal(value, inputErr)\n}\n\n// If a max width is defined, perform some logic to treat the visible area\n// as a horizontally scrolling viewport.\nfunc (m *Model) handleOverflow() {\n\tif m.Width() <= 0 || uniseg.StringWidth(string(m.value)) <= m.Width() {\n\t\tm.offset = 0\n\t\tm.offsetRight = len(m.value)\n\t\treturn\n\t}\n\n\t// Correct right offset if we've deleted characters\n\tm.offsetRight = min(m.offsetRight, len(m.value))\n\n\tif m.pos < m.offset {\n\t\tm.offset = m.pos\n\n\t\tw := 0\n\t\ti := 0\n\t\trunes := m.value[m.offset:]\n\n\t\tfor i < len(runes) && w <= m.Width() {\n\t\t\tw += rw.RuneWidth(runes[i])\n\t\t\tif w <= m.Width()+1 {\n\t\t\t\ti++\n\t\t\t}\n\t\t}\n\n\t\tm.offsetRight = m.offset + i\n\t} else if m.pos >= m.offsetRight {\n\t\tm.offsetRight = m.pos\n\n\t\tw := 0\n\t\trunes := m.value[:m.offsetRight]\n\t\ti := len(runes) - 1\n\n\t\tfor i > 0 && w < m.Width() {\n\t\t\tw += rw.RuneWidth(runes[i])\n\t\t\tif w <= m.Width() {\n\t\t\t\ti--\n\t\t\t}\n\t\t}\n\n\t\tm.offset = m.offsetRight - (len(runes) - 1 - i)\n\t}\n}\n\n// deleteBeforeCursor deletes all text before the cursor.\nfunc (m *Model) deleteBeforeCursor() {\n\tm.value = m.value[m.pos:]\n\tm.Err = m.validate(m.value)\n\tm.offset = 0\n\tm.SetCursor(0)\n}\n\n// deleteAfterCursor deletes all text after the cursor. If input is masked\n// delete everything after the cursor so as not to reveal word breaks in the\n// masked input.\nfunc (m *Model) deleteAfterCursor() {\n\tm.value = m.value[:m.pos]\n\tm.Err = m.validate(m.value)\n\tm.SetCursor(len(m.value))\n}\n\n// deleteWordBackward deletes the word left to the cursor.\nfunc (m *Model) deleteWordBackward() {\n\tif m.pos == 0 || len(m.value) == 0 {\n\t\treturn\n\t}\n\n\tif m.EchoMode != EchoNormal {\n\t\tm.deleteBeforeCursor()\n\t\treturn\n\t}\n\n\t// Linter note: it's critical that we acquire the initial cursor position\n\t// here prior to altering it via SetCursor() below. As such, moving this\n\t// call into the corresponding if clause does not apply here.\n\toldPos := m.pos\n\n\tm.SetCursor(m.pos - 1)\n\tfor unicode.IsSpace(m.value[m.pos]) {\n\t\tif m.pos <= 0 {\n\t\t\tbreak\n\t\t}\n\t\t// ignore series of whitespace before cursor\n\t\tm.SetCursor(m.pos - 1)\n\t}\n\n\tfor m.pos > 0 {\n\t\tif !unicode.IsSpace(m.value[m.pos]) {\n\t\t\tm.SetCursor(m.pos - 1)\n\t\t} else {\n\t\t\tif m.pos > 0 {\n\t\t\t\t// keep the previous space\n\t\t\t\tm.SetCursor(m.pos + 1)\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif oldPos > len(m.value) {\n\t\tm.value = m.value[:m.pos]\n\t} else {\n\t\tm.value = append(m.value[:m.pos], m.value[oldPos:]...)\n\t}\n\tm.Err = m.validate(m.value)\n}\n\n// deleteWordForward deletes the word right to the cursor. If input is masked\n// delete everything after the cursor so as not to reveal word breaks in the\n// masked input.\nfunc (m *Model) deleteWordForward() {\n\tif m.pos >= len(m.value) || len(m.value) == 0 {\n\t\treturn\n\t}\n\n\tif m.EchoMode != EchoNormal {\n\t\tm.deleteAfterCursor()\n\t\treturn\n\t}\n\n\toldPos := m.pos\n\tm.SetCursor(m.pos + 1)\n\tfor unicode.IsSpace(m.value[m.pos]) {\n\t\t// ignore series of whitespace after cursor\n\t\tm.SetCursor(m.pos + 1)\n\n\t\tif m.pos >= len(m.value) {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tfor m.pos < len(m.value) {\n\t\tif !unicode.IsSpace(m.value[m.pos]) {\n\t\t\tm.SetCursor(m.pos + 1)\n\t\t} else {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif m.pos > len(m.value) {\n\t\tm.value = m.value[:oldPos]\n\t} else {\n\t\tm.value = append(m.value[:oldPos], m.value[m.pos:]...)\n\t}\n\tm.Err = m.validate(m.value)\n\n\tm.SetCursor(oldPos)\n}\n\n// wordBackward moves the cursor one word to the left. If input is masked, move\n// input to the start so as not to reveal word breaks in the masked input.\nfunc (m *Model) wordBackward() {\n\tif m.pos == 0 || len(m.value) == 0 {\n\t\treturn\n\t}\n\n\tif m.EchoMode != EchoNormal {\n\t\tm.CursorStart()\n\t\treturn\n\t}\n\n\ti := m.pos - 1\n\tfor i >= 0 {\n\t\tif unicode.IsSpace(m.value[i]) {\n\t\t\tm.SetCursor(m.pos - 1)\n\t\t\ti--\n\t\t} else {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tfor i >= 0 {\n\t\tif !unicode.IsSpace(m.value[i]) {\n\t\t\tm.SetCursor(m.pos - 1)\n\t\t\ti--\n\t\t} else {\n\t\t\tbreak\n\t\t}\n\t}\n}\n\n// wordForward moves the cursor one word to the right. If the input is masked,\n// move input to the end so as not to reveal word breaks in the masked input.\nfunc (m *Model) wordForward() {\n\tif m.pos >= len(m.value) || len(m.value) == 0 {\n\t\treturn\n\t}\n\n\tif m.EchoMode != EchoNormal {\n\t\tm.CursorEnd()\n\t\treturn\n\t}\n\n\ti := m.pos\n\tfor i < len(m.value) {\n\t\tif unicode.IsSpace(m.value[i]) {\n\t\t\tm.SetCursor(m.pos + 1)\n\t\t\ti++\n\t\t} else {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tfor i < len(m.value) {\n\t\tif !unicode.IsSpace(m.value[i]) {\n\t\t\tm.SetCursor(m.pos + 1)\n\t\t\ti++\n\t\t} else {\n\t\t\tbreak\n\t\t}\n\t}\n}\n\nfunc (m Model) echoTransform(v string) string {\n\tswitch m.EchoMode {\n\tcase EchoPassword:\n\t\treturn strings.Repeat(string(m.EchoCharacter), uniseg.StringWidth(v))\n\tcase EchoNone:\n\t\treturn \"\"\n\tcase EchoNormal:\n\t\treturn v\n\tdefault:\n\t\treturn v\n\t}\n}\n\n// Update is the Bubble Tea update loop.\nfunc (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {\n\tif !m.focus {\n\t\treturn m, nil\n\t}\n\n\t// Need to check for completion before, because key is configurable and might be double assigned\n\tkeyMsg, ok := msg.(tea.KeyPressMsg)\n\tif ok && key.Matches(keyMsg, m.KeyMap.AcceptSuggestion) {\n\t\tif m.canAcceptSuggestion() {\n\t\t\tm.value = append(m.value, m.matchedSuggestions[m.currentSuggestionIndex][len(m.value):]...)\n\t\t\tm.CursorEnd()\n\t\t}\n\t}\n\n\t// Let's remember where the position of the cursor currently is so that if\n\t// the cursor position changes, we can reset the blink.\n\toldPos := m.pos\n\n\tswitch msg := msg.(type) {\n\tcase tea.KeyPressMsg:\n\t\tswitch {\n\t\tcase key.Matches(msg, m.KeyMap.DeleteWordBackward):\n\t\t\tm.deleteWordBackward()\n\t\tcase key.Matches(msg, m.KeyMap.DeleteCharacterBackward):\n\t\t\tm.Err = nil\n\t\t\tif len(m.value) > 0 {\n\t\t\t\tm.value = append(m.value[:max(0, m.pos-1)], m.value[m.pos:]...)\n\t\t\t\tm.Err = m.validate(m.value)\n\t\t\t\tif m.pos > 0 {\n\t\t\t\t\tm.SetCursor(m.pos - 1)\n\t\t\t\t}\n\t\t\t}\n\t\tcase key.Matches(msg, m.KeyMap.WordBackward):\n\t\t\tm.wordBackward()\n\t\tcase key.Matches(msg, m.KeyMap.CharacterBackward):\n\t\t\tif m.pos > 0 {\n\t\t\t\tm.SetCursor(m.pos - 1)\n\t\t\t}\n\t\tcase key.Matches(msg, m.KeyMap.WordForward):\n\t\t\tm.wordForward()\n\t\tcase key.Matches(msg, m.KeyMap.CharacterForward):\n\t\t\tif m.pos < len(m.value) {\n\t\t\t\tm.SetCursor(m.pos + 1)\n\t\t\t}\n\t\tcase key.Matches(msg, m.KeyMap.LineStart):\n\t\t\tm.CursorStart()\n\t\tcase key.Matches(msg, m.KeyMap.DeleteCharacterForward):\n\t\t\tif len(m.value) > 0 && m.pos < len(m.value) {\n\t\t\t\tm.value = slices.Delete(m.value, m.pos, m.pos+1)\n\t\t\t\tm.Err = m.validate(m.value)\n\t\t\t}\n\t\tcase key.Matches(msg, m.KeyMap.LineEnd):\n\t\t\tm.CursorEnd()\n\t\tcase key.Matches(msg, m.KeyMap.DeleteAfterCursor):\n\t\t\tm.deleteAfterCursor()\n\t\tcase key.Matches(msg, m.KeyMap.DeleteBeforeCursor):\n\t\t\tm.deleteBeforeCursor()\n\t\tcase key.Matches(msg, m.KeyMap.Paste):\n\t\t\treturn m, Paste\n\t\tcase key.Matches(msg, m.KeyMap.DeleteWordForward):\n\t\t\tm.deleteWordForward()\n\t\tcase key.Matches(msg, m.KeyMap.NextSuggestion):\n\t\t\tm.nextSuggestion()\n\t\tcase key.Matches(msg, m.KeyMap.PrevSuggestion):\n\t\t\tm.previousSuggestion()\n\t\tdefault:\n\t\t\t// Input one or more regular characters.\n\t\t\tm.insertRunesFromUserInput([]rune(msg.Text))\n\t\t}\n\n\t\t// Check again if can be completed\n\t\t// because value might be something that does not match the completion prefix\n\t\tm.updateSuggestions()\n\n\tcase tea.PasteMsg:\n\t\tm.insertRunesFromUserInput([]rune(msg.Content))\n\n\tcase pasteMsg:\n\t\tm.insertRunesFromUserInput([]rune(msg))\n\n\tcase pasteErrMsg:\n\t\tm.Err = msg\n\t}\n\n\tvar cmds []tea.Cmd\n\tvar cmd tea.Cmd\n\n\tif m.useVirtualCursor {\n\t\tm.virtualCursor, cmd = m.virtualCursor.Update(msg)\n\t\tcmds = append(cmds, cmd)\n\n\t\t// If the cursor position changed, reset the blink state. This is a\n\t\t// small UX nuance that makes cursor movement obvious and feel snappy.\n\t\tif oldPos != m.pos && m.virtualCursor.Mode() == cursor.CursorBlink {\n\t\t\tm.virtualCursor.IsBlinked = false\n\t\t\tcmds = append(cmds, m.virtualCursor.Blink())\n\t\t}\n\t}\n\n\tm.handleOverflow()\n\treturn m, tea.Batch(cmds...)\n}\n\n// View renders the textinput in its current state.\nfunc (m Model) View() string {\n\t// Placeholder text\n\tif len(m.value) == 0 && m.Placeholder != \"\" {\n\t\treturn m.placeholderView()\n\t}\n\n\tstyles := m.activeStyle()\n\n\tstyleText := styles.Text.Inline(true).Render\n\n\tvalue := m.value[m.offset:m.offsetRight]\n\tpos := max(0, m.pos-m.offset)\n\tv := styleText(m.echoTransform(string(value[:pos])))\n\n\tif pos < len(value) { //nolint:nestif\n\t\tchar := m.echoTransform(string(value[pos]))\n\t\tm.virtualCursor.SetChar(char)\n\t\tv += m.virtualCursor.View()                            // cursor and text under it\n\t\tv += styleText(m.echoTransform(string(value[pos+1:]))) // text after cursor\n\t\tv += m.completionView(0)                               // suggested completion\n\t} else {\n\t\tif m.focus && m.canAcceptSuggestion() {\n\t\t\tsuggestion := m.matchedSuggestions[m.currentSuggestionIndex]\n\t\t\tif len(value) < len(suggestion) {\n\t\t\t\tm.virtualCursor.TextStyle = styles.Suggestion\n\t\t\t\tm.virtualCursor.SetChar(m.echoTransform(string(suggestion[pos])))\n\t\t\t\tv += m.virtualCursor.View()\n\t\t\t\tv += m.completionView(1)\n\t\t\t} else {\n\t\t\t\tm.virtualCursor.SetChar(\" \")\n\t\t\t\tv += m.virtualCursor.View()\n\t\t\t}\n\t\t} else {\n\t\t\tm.virtualCursor.SetChar(\" \")\n\t\t\tv += m.virtualCursor.View()\n\t\t}\n\t}\n\n\t// If a max width and background color were set fill the empty spaces with\n\t// the background color.\n\tvalWidth := uniseg.StringWidth(string(value))\n\tif m.Width() > 0 && valWidth <= m.Width() {\n\t\tpadding := max(0, m.Width()-valWidth)\n\t\tif valWidth+padding <= m.Width() && pos < len(value) {\n\t\t\tpadding++\n\t\t}\n\t\tv += styleText(strings.Repeat(\" \", padding))\n\t}\n\n\treturn m.promptView() + v\n}\n\nfunc (m Model) promptView() string {\n\treturn m.activeStyle().Prompt.Render(m.Prompt)\n}\n\n// placeholderView returns the prompt and placeholder view, if any.\nfunc (m Model) placeholderView() string {\n\tvar (\n\t\tv      string\n\t\tstyles = m.activeStyle()\n\t\trender = styles.Placeholder.Render\n\t)\n\n\tp := make([]rune, m.Width()+1)\n\tcopy(p, []rune(m.Placeholder))\n\n\tm.virtualCursor.TextStyle = styles.Placeholder\n\tm.virtualCursor.SetChar(string(p[:1]))\n\tv += m.virtualCursor.View()\n\n\t// If the entire placeholder is already set and no padding is needed, finish\n\tif m.Width() < 1 && len(p) <= 1 {\n\t\treturn styles.Prompt.Render(m.Prompt) + v\n\t}\n\n\t// If Width is set then size placeholder accordingly\n\tif m.Width() > 0 {\n\t\t// available width is width - len + cursor offset of 1\n\t\tminWidth := lipgloss.Width(m.Placeholder)\n\t\tavailWidth := m.Width() - minWidth + 1\n\n\t\t// if width < len, 'subtract'(add) number to len and dont add padding\n\t\tif availWidth < 0 {\n\t\t\tminWidth += availWidth\n\t\t\tavailWidth = 0\n\t\t}\n\t\t// append placeholder[len] - cursor, append padding\n\t\tv += render(string(p[1:minWidth]))\n\t\tv += render(strings.Repeat(\" \", availWidth))\n\t} else {\n\t\t// if there is no width, the placeholder can be any length\n\t\tv += render(string(p[1:]))\n\t}\n\n\treturn styles.Prompt.Render(m.Prompt) + v\n}\n\n// Blink is a command used to initialize cursor blinking.\nfunc Blink() tea.Msg {\n\treturn cursor.Blink()\n}\n\n// Paste is a command for pasting from the clipboard into the text input.\nfunc Paste() tea.Msg {\n\tstr, err := clipboard.ReadAll()\n\tif err != nil {\n\t\treturn pasteErrMsg{err}\n\t}\n\treturn pasteMsg(str)\n}\n\nfunc clamp(v, low, high int) int {\n\tif high < low {\n\t\tlow, high = high, low\n\t}\n\treturn min(high, max(low, v))\n}\n\nfunc (m Model) completionView(offset int) string {\n\tif !m.canAcceptSuggestion() {\n\t\treturn \"\"\n\t}\n\tvalue := m.value\n\tsuggestion := m.matchedSuggestions[m.currentSuggestionIndex]\n\tif len(value) < len(suggestion) {\n\t\treturn m.activeStyle().Suggestion.Inline(true).\n\t\t\tRender(string(suggestion[len(value)+offset:]))\n\t}\n\treturn \"\"\n}\n\nfunc (m *Model) getSuggestions(sugs [][]rune) []string {\n\tsuggestions := make([]string, len(sugs))\n\tfor i, s := range sugs {\n\t\tsuggestions[i] = string(s)\n\t}\n\treturn suggestions\n}\n\n// AvailableSuggestions returns the list of available suggestions.\nfunc (m *Model) AvailableSuggestions() []string {\n\treturn m.getSuggestions(m.suggestions)\n}\n\n// MatchedSuggestions returns the list of matched suggestions.\nfunc (m *Model) MatchedSuggestions() []string {\n\treturn m.getSuggestions(m.matchedSuggestions)\n}\n\n// CurrentSuggestionIndex returns the currently selected suggestion index.\nfunc (m *Model) CurrentSuggestionIndex() int {\n\treturn m.currentSuggestionIndex\n}\n\n// CurrentSuggestion returns the currently selected suggestion.\nfunc (m *Model) CurrentSuggestion() string {\n\tif m.currentSuggestionIndex >= len(m.matchedSuggestions) {\n\t\treturn \"\"\n\t}\n\n\treturn string(m.matchedSuggestions[m.currentSuggestionIndex])\n}\n\n// canAcceptSuggestion returns whether there is an acceptable suggestion to\n// autocomplete the current value.\nfunc (m *Model) canAcceptSuggestion() bool {\n\treturn len(m.matchedSuggestions) > 0\n}\n\n// updateSuggestions refreshes the list of matching suggestions.\nfunc (m *Model) updateSuggestions() {\n\tif !m.ShowSuggestions {\n\t\treturn\n\t}\n\n\tif len(m.value) <= 0 || len(m.suggestions) <= 0 {\n\t\tm.matchedSuggestions = [][]rune{}\n\t\treturn\n\t}\n\n\tmatches := [][]rune{}\n\tfor _, s := range m.suggestions {\n\t\tsuggestion := string(s)\n\n\t\tif strings.HasPrefix(strings.ToLower(suggestion), strings.ToLower(string(m.value))) {\n\t\t\tmatches = append(matches, []rune(suggestion))\n\t\t}\n\t}\n\tif !reflect.DeepEqual(matches, m.matchedSuggestions) {\n\t\tm.currentSuggestionIndex = 0\n\t}\n\n\tm.matchedSuggestions = matches\n}\n\n// nextSuggestion selects the next suggestion.\nfunc (m *Model) nextSuggestion() {\n\tm.currentSuggestionIndex = (m.currentSuggestionIndex + 1)\n\tif m.currentSuggestionIndex >= len(m.matchedSuggestions) {\n\t\tm.currentSuggestionIndex = 0\n\t}\n}\n\n// previousSuggestion selects the previous suggestion.\nfunc (m *Model) previousSuggestion() {\n\tm.currentSuggestionIndex = (m.currentSuggestionIndex - 1)\n\tif m.currentSuggestionIndex < 0 {\n\t\tm.currentSuggestionIndex = len(m.matchedSuggestions) - 1\n\t}\n}\n\nfunc (m Model) validate(v []rune) error {\n\tif m.Validate != nil {\n\t\treturn m.Validate(string(v))\n\t}\n\treturn nil\n}\n\n// Cursor returns a [tea.Cursor] for rendering a real cursor in a Bubble Tea\n// program. This requires that [Model.VirtualCursor] is set to false.\n//\n// Note that you will almost certainly also need to adjust the offset cursor\n// position per the textarea's per the textarea's position in the terminal.\n//\n// Example:\n//\n//\t// In your top-level View function:\n//\tf := tea.NewFrame(m.textarea.View())\n//\tf.Cursor = m.textarea.Cursor()\n//\tf.Cursor.Position.X += offsetX\n//\tf.Cursor.Position.Y += offsetY\nfunc (m Model) Cursor() *tea.Cursor {\n\tif m.useVirtualCursor || !m.Focused() {\n\t\treturn nil\n\t}\n\n\tw := lipgloss.Width\n\n\tpromptWidth := w(m.promptView())\n\txOffset := m.Position() +\n\t\tpromptWidth\n\tif m.width > 0 {\n\t\txOffset = min(xOffset, m.width+promptWidth)\n\t}\n\n\tstyle := m.styles.Cursor\n\tc := tea.NewCursor(xOffset, 0)\n\tc.Blink = style.Blink\n\tc.Color = style.Color\n\tc.Shape = style.Shape\n\treturn c\n}\n\n// updateVirtualCursorStyle sets styling on the virtual cursor based on the\n// textarea's style settings.\nfunc (m *Model) updateVirtualCursorStyle() {\n\tif !m.useVirtualCursor {\n\t\t// Hide the virtual cursor if we're using a real cursor.\n\t\tm.virtualCursor.SetMode(cursor.CursorHide)\n\t\treturn\n\t}\n\n\tm.virtualCursor.Style = lipgloss.NewStyle().Foreground(m.styles.Cursor.Color)\n\n\t// By default, the blink speed of the cursor is set to a default\n\t// internally.\n\tif m.styles.Cursor.Blink {\n\t\tif m.styles.Cursor.BlinkSpeed > 0 {\n\t\t\tm.virtualCursor.BlinkSpeed = m.styles.Cursor.BlinkSpeed\n\t\t}\n\t\tm.virtualCursor.SetMode(cursor.CursorBlink)\n\t\treturn\n\t}\n\tm.virtualCursor.SetMode(cursor.CursorStatic)\n}\n\n// activeStyle returns the appropriate set of styles to use depending on\n// whether the textarea is focused or blurred.\nfunc (m Model) activeStyle() *StyleState {\n\tif m.focus {\n\t\treturn &m.styles.Focused\n\t}\n\treturn &m.styles.Blurred\n}\n"
  },
  {
    "path": "textinput/textinput_test.go",
    "content": "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_CurrentSuggestion(t *testing.T) {\n\ttextinput := New()\n\ttextinput.ShowSuggestions = true\n\n\tsuggestion := textinput.CurrentSuggestion()\n\texpected := \"\"\n\tif suggestion != expected {\n\t\tt.Fatalf(\"Error: expected no current suggestion but was %s\", suggestion)\n\t}\n\n\ttextinput.SetSuggestions([]string{\"test1\", \"test2\", \"test3\"})\n\tsuggestion = textinput.CurrentSuggestion()\n\texpected = \"\"\n\tif suggestion != expected {\n\t\tt.Fatalf(\"Error: expected no current suggestion but was %s\", suggestion)\n\t}\n\n\ttextinput.SetValue(\"test\")\n\ttextinput.updateSuggestions()\n\ttextinput.nextSuggestion()\n\tsuggestion = textinput.CurrentSuggestion()\n\texpected = \"test2\"\n\tif suggestion != expected {\n\t\tt.Fatalf(\"Error: expected first suggestion but was %s\", suggestion)\n\t}\n\n\ttextinput.Blur()\n\tif strings.HasSuffix(textinput.View(), \"test2\") {\n\t\tt.Fatalf(\"Error: suggestions should not be rendered when input isn't focused. expected \\\"> test\\\" but got \\\"%s\\\"\", textinput.View())\n\t}\n}\n\nfunc Test_SlicingOutsideCap(t *testing.T) {\n\ttextinput := New()\n\ttextinput.Placeholder = \"作業ディレクトリを指定してください\"\n\ttextinput.SetWidth(32)\n\ttextinput.View()\n}\n\nfunc TestChinesePlaceholder(t *testing.T) {\n\tt.Skip(\"Skipping flaky test, the returned view seems incorrect. TODO: Needs investigation.\")\n\ttextinput := New()\n\ttextinput.Placeholder = \"输入消息...\"\n\ttextinput.SetWidth(20)\n\n\tgot := textinput.View()\n\texpected := \"> 输入消息...       \"\n\tif got != expected {\n\t\tt.Fatalf(\"expected %q but got %q\", expected, got)\n\t}\n}\n\nfunc TestPlaceholderTruncate(t *testing.T) {\n\tt.Skip(\"Skipping flaky test, the returned view seems incorrect. TODO: Needs investigation.\")\n\ttextinput := New()\n\ttextinput.Placeholder = \"A very long placeholder, or maybe not so much\"\n\ttextinput.SetWidth(10)\n\n\tgot := textinput.View()\n\texpected := \"> A very …\"\n\tif got != expected {\n\t\tt.Fatalf(\"expected %q but got %q\", expected, got)\n\t}\n}\n\nfunc ExampleValidateFunc() {\n\tcreditCardNumber := New()\n\tcreditCardNumber.Placeholder = \"4505 **** **** 1234\"\n\tcreditCardNumber.Focus()\n\tcreditCardNumber.CharLimit = 20\n\tcreditCardNumber.SetWidth(30)\n\tcreditCardNumber.Prompt = \"\"\n\t// This anonymous function is a valid function for ValidateFunc.\n\tcreditCardNumber.Validate = func(s string) error {\n\t\t// Credit Card Number should a string less than 20 digits\n\t\t// It should include 16 integers and 3 spaces\n\t\tif len(s) > 16+3 {\n\t\t\treturn fmt.Errorf(\"CCN is too long\")\n\t\t}\n\n\t\tif len(s) == 0 || len(s)%5 != 0 && (s[len(s)-1] < '0' || s[len(s)-1] > '9') {\n\t\t\treturn fmt.Errorf(\"CCN is invalid\")\n\t\t}\n\n\t\t// The last digit should be a number unless it is a multiple of 4 in which\n\t\t// case it should be a space\n\t\tif len(s)%5 == 0 && s[len(s)-1] != ' ' {\n\t\t\treturn fmt.Errorf(\"CCN must separate groups with spaces\")\n\t\t}\n\n\t\t// The remaining digits should be integers\n\t\tc := strings.ReplaceAll(s, \" \", \"\")\n\t\t_, err := strconv.ParseInt(c, 10, 64)\n\n\t\treturn err\n\t}\n}\n\nfunc keyPress(key rune) tea.Msg {\n\treturn tea.KeyPressMsg{Code: key, Text: string(key)}\n}\n\nfunc sendString(m Model, str string) Model {\n\tfor _, k := range str {\n\t\tm, _ = m.Update(keyPress(k))\n\t}\n\n\treturn m\n}\n"
  },
  {
    "path": "timer/timer.go",
    "content": "// Package timer provides a simple timeout component.\npackage timer\n\nimport (\n\t\"sync/atomic\"\n\t\"time\"\n\n\ttea \"charm.land/bubbletea/v2\"\n)\n\nvar lastID int64\n\nfunc nextID() int {\n\treturn int(atomic.AddInt64(&lastID, 1))\n}\n\n// Option is a configuration option in [New]. For example:\n//\n//\ttimer := New(time.Second*10, WithInterval(5*time.Second))\ntype Option func(*Model)\n\n// WithInterval is an option for setting the interval between ticks. Pass as\n// an argument to [New].\nfunc WithInterval(interval time.Duration) Option {\n\treturn func(m *Model) {\n\t\tm.Interval = interval\n\t}\n}\n\n// Authors note with regard to start and stop commands:\n//\n// Technically speaking, sending commands to start and stop the timer in this\n// case is extraneous. To stop the timer we'd just need to set the 'running'\n// property on the model to false which cause logic in the update function to\n// stop responding to TickMsgs. To start the model we'd set 'running' to true\n// and fire off a TickMsg. Helper functions would look like:\n//\n//     func (m *model) Start() tea.Cmd\n//     func (m *model) Stop()\n//\n// The danger with this approach, however, is that order of operations becomes\n// important with helper functions like the above. Consider the following:\n//\n//     // Would not work\n//     return m, m.timer.Start()\n//\n//\t   // Would work\n//     cmd := m.timer.start()\n//     return m, cmd\n//\n// Thus, because of potential pitfalls like the ones above, we've introduced\n// the extraneous StartStopMsg to simplify the mental model when using this\n// package. Bear in mind that the practice of sending commands to simply\n// communicate with other parts of your application, such as in this package,\n// is still not recommended.\n\n// StartStopMsg is used to start and stop the timer.\ntype StartStopMsg struct {\n\tID      int\n\trunning bool\n}\n\n// TickMsg is a message that is sent on every timer tick.\ntype TickMsg struct {\n\t// ID is the identifier of the timer that sends the message. This makes\n\t// it possible to determine which timer a tick belongs to when there\n\t// are multiple timers running.\n\t//\n\t// Note, however, that a timer will reject ticks from other timers, so\n\t// it's safe to flow all TickMsgs through all timers and have them still\n\t// behave appropriately.\n\tID int\n\n\t// Timeout returns whether or not this tick is a timeout tick. You can\n\t// alternatively listen for TimeoutMsg.\n\tTimeout bool\n\n\ttag int\n}\n\n// TimeoutMsg is a message that is sent once when the timer times out.\n//\n// It's a convenience message sent alongside a TickMsg with the Timeout value\n// set to true.\ntype TimeoutMsg struct {\n\tID int\n}\n\n// Model of the timer component.\ntype Model struct {\n\t// How long until the timer expires.\n\tTimeout time.Duration\n\n\t// How long to wait before every tick. Defaults to 1 second.\n\tInterval time.Duration\n\n\tid      int\n\ttag     int\n\trunning bool\n}\n\n// New creates a new timer with the given timeout and default 1s interval.\nfunc New(timeout time.Duration, opts ...Option) Model {\n\tm := Model{\n\t\tTimeout:  timeout,\n\t\tInterval: time.Second,\n\t\trunning:  true,\n\t\tid:       nextID(),\n\t}\n\tfor _, opt := range opts {\n\t\topt(&m)\n\t}\n\treturn m\n}\n\n// ID returns the model's identifier. This can be used to determine if messages\n// belong to this timer instance when there are multiple timers.\nfunc (m Model) ID() int {\n\treturn m.id\n}\n\n// Running returns whether or not the timer is running. If the timer has timed\n// out this will always return false.\nfunc (m Model) Running() bool {\n\tif m.Timedout() || !m.running {\n\t\treturn false\n\t}\n\treturn true\n}\n\n// Timedout returns whether or not the timer has timed out.\nfunc (m Model) Timedout() bool {\n\treturn m.Timeout <= 0\n}\n\n// Init starts the timer.\nfunc (m Model) Init() tea.Cmd {\n\treturn m.tick()\n}\n\n// Update handles the timer tick.\nfunc (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {\n\tswitch msg := msg.(type) {\n\tcase StartStopMsg:\n\t\tif msg.ID != 0 && msg.ID != m.id {\n\t\t\treturn m, nil\n\t\t}\n\t\tm.running = msg.running\n\t\treturn m, m.tick()\n\tcase TickMsg:\n\t\tif !m.Running() || (msg.ID != 0 && msg.ID != m.id) {\n\t\t\tbreak\n\t\t}\n\n\t\t// If a tag is set, and it's not the one we expect, reject the message.\n\t\t// This prevents the ticker from receiving too many messages and\n\t\t// thus ticking too fast.\n\t\tif msg.tag > 0 && msg.tag != m.tag {\n\t\t\treturn m, nil\n\t\t}\n\n\t\tm.Timeout -= m.Interval\n\t\treturn m, tea.Batch(m.tick(), m.timedout())\n\t}\n\n\treturn m, nil\n}\n\n// View of the timer component.\nfunc (m Model) View() string {\n\treturn m.Timeout.String()\n}\n\n// Start resumes the timer. Has no effect if the timer has timed out.\nfunc (m *Model) Start() tea.Cmd {\n\treturn m.startStop(true)\n}\n\n// Stop pauses the timer. Has no effect if the timer has timed out.\nfunc (m *Model) Stop() tea.Cmd {\n\treturn m.startStop(false)\n}\n\n// Toggle stops the timer if it's running and starts it if it's stopped.\nfunc (m *Model) Toggle() tea.Cmd {\n\treturn m.startStop(!m.Running())\n}\n\nfunc (m Model) tick() tea.Cmd {\n\treturn tea.Tick(m.Interval, func(_ time.Time) tea.Msg {\n\t\treturn TickMsg{ID: m.id, tag: m.tag, Timeout: m.Timedout()}\n\t})\n}\n\nfunc (m Model) timedout() tea.Cmd {\n\tif !m.Timedout() {\n\t\treturn nil\n\t}\n\treturn func() tea.Msg {\n\t\treturn TimeoutMsg{ID: m.id}\n\t}\n}\n\nfunc (m Model) startStop(v bool) tea.Cmd {\n\treturn func() tea.Msg {\n\t\treturn StartStopMsg{ID: m.id, running: v}\n\t}\n}\n"
  },
  {
    "path": "viewport/highlight.go",
    "content": "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// parseMatches converts the given matches into highlight ranges.\n//\n// Assumptions:\n// - matches are measured in bytes, e.g. what [regex.FindAllStringIndex] would return\n// - matches were made against the given content\n// - matches are in order\n// - matches do not overlap\n// - content is line terminated with \\n only\n//\n// We'll then convert the ranges into [highlightInfo]s, which hold the starting\n// line and the grapheme positions.\nfunc parseMatches(\n\tcontent string,\n\tmatches [][]int,\n) []highlightInfo {\n\tif len(matches) == 0 {\n\t\treturn nil\n\t}\n\n\tline := 0\n\tgraphemePos := 0\n\tpreviousLinesOffset := 0\n\tbytePos := 0\n\n\thighlights := make([]highlightInfo, 0, len(matches))\n\tgr := uniseg.NewGraphemes(ansi.Strip(content))\n\n\tfor _, match := range matches {\n\t\tbyteStart, byteEnd := match[0], match[1]\n\n\t\t// hilight for this match:\n\t\thi := highlightInfo{\n\t\t\tlines: map[int][2]int{},\n\t\t}\n\n\t\t// find the beginning of this byte range, setup current line and\n\t\t// grapheme position.\n\t\tfor byteStart > bytePos {\n\t\t\tif !gr.Next() {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif content[bytePos] == '\\n' {\n\t\t\t\tpreviousLinesOffset = graphemePos + 1\n\t\t\t\tline++\n\t\t\t}\n\t\t\tgraphemePos += max(1, gr.Width())\n\t\t\tbytePos += len(gr.Str())\n\t\t}\n\n\t\thi.lineStart = line\n\t\thi.lineEnd = line\n\n\t\tgraphemeStart := graphemePos\n\n\t\t// loop until we find the end\n\t\tfor byteEnd > bytePos {\n\t\t\tif !gr.Next() {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\t// if it ends with a new line, add the range, increase line, and continue\n\t\t\tif content[bytePos] == '\\n' {\n\t\t\t\tcolstart := max(0, graphemeStart-previousLinesOffset)\n\t\t\t\tcolend := max(graphemePos-previousLinesOffset+1, colstart) // +1 its \\n itself\n\n\t\t\t\tif colend > colstart {\n\t\t\t\t\thi.lines[line] = [2]int{colstart, colend}\n\t\t\t\t\thi.lineEnd = line\n\t\t\t\t}\n\n\t\t\t\tpreviousLinesOffset = graphemePos + 1\n\t\t\t\tline++\n\t\t\t}\n\n\t\t\tgraphemePos += max(1, gr.Width())\n\t\t\tbytePos += len(gr.Str())\n\t\t}\n\n\t\t// we found it!, add highlight and continue\n\t\tif bytePos == byteEnd {\n\t\t\tcolstart := max(0, graphemeStart-previousLinesOffset)\n\t\t\tcolend := max(graphemePos-previousLinesOffset, colstart)\n\n\t\t\tif colend > colstart {\n\t\t\t\thi.lines[line] = [2]int{colstart, colend}\n\t\t\t\thi.lineEnd = line\n\t\t\t}\n\t\t}\n\n\t\thighlights = append(highlights, hi)\n\t}\n\n\treturn highlights\n}\n\ntype highlightInfo struct {\n\t// in which line this highlight starts and ends\n\tlineStart, lineEnd int\n\n\t// the grapheme highlight ranges for each of these lines\n\tlines map[int][2]int\n}\n\n// coords returns the line x column of this highlight.\nfunc (hi highlightInfo) coords() (int, int, int) {\n\tfor i := hi.lineStart; i <= hi.lineEnd; i++ {\n\t\thl, ok := hi.lines[i]\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\treturn i, hl[0], hl[1]\n\t}\n\treturn hi.lineStart, 0, 0\n}\n\nfunc makeHighlightRanges(\n\thighlights []highlightInfo,\n\tline int,\n\tstyle lipgloss.Style,\n) []lipgloss.Range {\n\tresult := []lipgloss.Range{}\n\tfor _, hi := range highlights {\n\t\tlihi, ok := hi.lines[line]\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\tif lihi == [2]int{} {\n\t\t\tcontinue\n\t\t}\n\t\tresult = append(result, lipgloss.NewRange(lihi[0], lihi[1], style))\n\t}\n\treturn result\n}\n"
  },
  {
    "path": "viewport/keymap.go",
    "content": "// Package viewport provides a component for rendering a viewport in a Bubble\n// Tea.\npackage viewport\n\nimport \"charm.land/bubbles/v2/key\"\n\n// KeyMap defines the keybindings for the viewport. Note that you don't\n// necessary need to use keybindings at all; the viewport can be controlled\n// programmatically with methods like Model.LineDown(1). See the GoDocs for\n// details.\ntype KeyMap struct {\n\tPageDown     key.Binding\n\tPageUp       key.Binding\n\tHalfPageUp   key.Binding\n\tHalfPageDown key.Binding\n\tDown         key.Binding\n\tUp           key.Binding\n\tLeft         key.Binding\n\tRight        key.Binding\n}\n\n// DefaultKeyMap returns a set of pager-like default keybindings.\nfunc DefaultKeyMap() KeyMap {\n\treturn KeyMap{\n\t\tPageDown: key.NewBinding(\n\t\t\tkey.WithKeys(\"pgdown\", \"space\", \"f\"),\n\t\t\tkey.WithHelp(\"f/pgdn\", \"page down\"),\n\t\t),\n\t\tPageUp: key.NewBinding(\n\t\t\tkey.WithKeys(\"pgup\", \"b\"),\n\t\t\tkey.WithHelp(\"b/pgup\", \"page up\"),\n\t\t),\n\t\tHalfPageUp: key.NewBinding(\n\t\t\tkey.WithKeys(\"u\", \"ctrl+u\"),\n\t\t\tkey.WithHelp(\"u\", \"½ page up\"),\n\t\t),\n\t\tHalfPageDown: key.NewBinding(\n\t\t\tkey.WithKeys(\"d\", \"ctrl+d\"),\n\t\t\tkey.WithHelp(\"d\", \"½ page down\"),\n\t\t),\n\t\tUp: key.NewBinding(\n\t\t\tkey.WithKeys(\"up\", \"k\"),\n\t\t\tkey.WithHelp(\"↑/k\", \"up\"),\n\t\t),\n\t\tDown: key.NewBinding(\n\t\t\tkey.WithKeys(\"down\", \"j\"),\n\t\t\tkey.WithHelp(\"↓/j\", \"down\"),\n\t\t),\n\t\tLeft: key.NewBinding(\n\t\t\tkey.WithKeys(\"left\", \"h\"),\n\t\t\tkey.WithHelp(\"←/h\", \"move left\"),\n\t\t),\n\t\tRight: key.NewBinding(\n\t\t\tkey.WithKeys(\"right\", \"l\"),\n\t\t\tkey.WithHelp(\"→/l\", \"move right\"),\n\t\t),\n\t}\n}\n"
  },
  {
    "path": "viewport/testdata/TestSizing/view-40x1-softwrap-at-bottom.golden",
    "content": "╭────────────────────────────────────────╮\n│ll know how many foes you've defeated.  │\n╰────────────────────────────────────────╯"
  },
  {
    "path": "viewport/testdata/TestSizing/view-40x1-softwrap-scrolled-plus-1.golden",
    "content": "╭────────────────────────────────────────╮\n│cter Zote from an awesome \"Hollow knight│\n╰────────────────────────────────────────╯"
  },
  {
    "path": "viewport/testdata/TestSizing/view-40x1-softwrap-scrolled-plus-2.golden",
    "content": "╭────────────────────────────────────────╮\n│\" game (https://store.steampowered.com/a│\n╰────────────────────────────────────────╯"
  },
  {
    "path": "viewport/testdata/TestSizing/view-40x1-softwrap.golden",
    "content": "╭────────────────────────────────────────╮\n│57 Precepts of narcissistic comedy chara│\n╰────────────────────────────────────────╯"
  },
  {
    "path": "viewport/testdata/TestSizing/view-40x1.golden",
    "content": "╭────────────────────────────────────────╮\n│57 Precepts of narcissistic comedy chara│\n╰────────────────────────────────────────╯"
  },
  {
    "path": "viewport/testdata/TestSizing/view-40x100percent.golden",
    "content": "╭──────────────────────────────────────╮\n│57 Precepts of narcissistic comedy cha│\n│Precept One: 'Always Win Your Battles'│\n│                                      │\n│Precept Two: 'Never Let Them Laugh at │\n│Precept Three: 'Always Be Rested'. Fig│\n│Precept Four: 'Forget Your Past'. The │\n│Precept Five: 'Strength Beats Strength│\n│Precept Six: 'Choose Your Own Fate'. O│\n│Precept Seven: 'Mourn Not the Dead'. W│\n│Precept Eight: 'Travel Alone'. You can│\n│Precept Nine: 'Keep Your Home Tidy'. Y│\n│Precept Ten: 'Keep Your Weapon Sharp'.│\n│Precept Eleven: 'Mothers Will Always B│\n│Precept Twelve: 'Keep Your Cloak Dry'.│\n│Precept Thirteen: 'Never Be Afraid'. F│\n│Precept Fourteen: 'Respect Your Superi│\n│Precept Fifteen: 'One Foe, One Blow'. │\n╰──────────────────────────────────────╯"
  },
  {
    "path": "viewport/testdata/TestSizing/view-50x15-content-lines.golden",
    "content": "57 Precepts of narcissistic comedy character Zote \nawesome \"Hollow knight\" game                      \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  "
  },
  {
    "path": "viewport/testdata/TestSizing/view-50x15-softwrap-at-bottom.golden",
    "content": "╭────────────────────────────────────────────────╮\n│Precept Thirteen: 'Never Be Afraid'. Fear can on│\n│ly hold you back. Facing your fears can be a tre│\n│mendous effort. Therefore, you should just not b│\n│e afraid in the first place.                    │\n│Precept Fourteen: 'Respect Your Superiors'. If s│\n│omeone is your superior in strength or intellect│\n│ or both, you need to show them your respect. Do│\n│n't ignore them or laugh at them.               │\n│Precept Fifteen: 'One Foe, One Blow'. You should│\n│ only use a single blow to defeat an enemy. Any │\n│more is a waste. Also, by counting your blows as│\n│ you fight, you'll know how many foes you've def│\n│eated.                                          │\n╰────────────────────────────────────────────────╯"
  },
  {
    "path": "viewport/testdata/TestSizing/view-50x15-softwrap-at-top.golden",
    "content": "╭────────────────────────────────────────────────╮\n│57 Precepts of narcissistic comedy character Zot│\n│e from an awesome \"Hollow knight\" game (https://│\n│store.steampowered.com/app/367520/Hollow_Knight/│\n│).                                              │\n│Precept One: 'Always Win Your Battles'. Losing a│\n│ battle earns you nothing and teaches you nothin│\n│g. Win your battles, or don't engage in them at │\n│all!                                            │\n│                                                │\n│Precept Two: 'Never Let Them Laugh at You'. Fool│\n│s laugh at everything, even at their superiors. │\n│But beware, laughter isn't harmless! Laughter sp│\n│reads like a disease, and soon everyone is laugh│\n╰────────────────────────────────────────────────╯"
  },
  {
    "path": "viewport/testdata/TestSizing/view-50x15-softwrap-gutter-at-bottom.golden",
    "content": "╭────────────────────────────────────────────────╮\n│  Precept Thirteen: 'Never Be Afraid'. Fear can │\n│  only hold you back. Facing your fears can be a│\n│   tremendous effort. Therefore, you should just│\n│   not be afraid in the first place.            │\n│  Precept Fourteen: 'Respect Your Superiors'. If│\n│   someone is your superior in strength or intel│\n│  lect or both, you need to show them your respe│\n│  ct. Don't ignore them or laugh at them.       │\n│  Precept Fifteen: 'One Foe, One Blow'. You shou│\n│  ld only use a single blow to defeat an enemy. │\n│  Any more is a waste. Also, by counting your bl│\n│  ows as you fight, you'll know how many foes yo│\n│  u've defeated.                                │\n╰────────────────────────────────────────────────╯"
  },
  {
    "path": "viewport/testdata/TestSizing/view-50x15-softwrap-gutter-at-top.golden",
    "content": "╭────────────────────────────────────────────────╮\n│  57 Precepts of narcissistic comedy character Z│\n│  ote from an awesome \"Hollow knight\" game (http│\n│  s://store.steampowered.com/app/367520/Hollow_K│\n│  night/).                                      │\n│  Precept One: 'Always Win Your Battles'. Losing│\n│   a battle earns you nothing and teaches you no│\n│  thing. Win your battles, or don't engage in th│\n│  em at all!                                    │\n│                                                │\n│  Precept Two: 'Never Let Them Laugh at You'. Fo│\n│  ols laugh at everything, even at their superio│\n│  rs. But beware, laughter isn't harmless! Laugh│\n│  ter spreads like a disease, and soon everyone │\n╰────────────────────────────────────────────────╯"
  },
  {
    "path": "viewport/testdata/TestSizing/view-50x15-softwrap-gutter-scrolled-plus-1.golden",
    "content": "╭────────────────────────────────────────────────╮\n│  ote from an awesome \"Hollow knight\" game (http│\n│  s://store.steampowered.com/app/367520/Hollow_K│\n│  night/).                                      │\n│  Precept One: 'Always Win Your Battles'. Losing│\n│   a battle earns you nothing and teaches you no│\n│  thing. Win your battles, or don't engage in th│\n│  em at all!                                    │\n│                                                │\n│  Precept Two: 'Never Let Them Laugh at You'. Fo│\n│  ols laugh at everything, even at their superio│\n│  rs. But beware, laughter isn't harmless! Laugh│\n│  ter spreads like a disease, and soon everyone │\n│  is laughing at you. You need to strike at the │\n╰────────────────────────────────────────────────╯"
  },
  {
    "path": "viewport/testdata/TestSizing/view-50x15-softwrap-gutter-scrolled-plus-2.golden",
    "content": "╭────────────────────────────────────────────────╮\n│  s://store.steampowered.com/app/367520/Hollow_K│\n│  night/).                                      │\n│  Precept One: 'Always Win Your Battles'. Losing│\n│   a battle earns you nothing and teaches you no│\n│  thing. Win your battles, or don't engage in th│\n│  em at all!                                    │\n│                                                │\n│  Precept Two: 'Never Let Them Laugh at You'. Fo│\n│  ols laugh at everything, even at their superio│\n│  rs. But beware, laughter isn't harmless! Laugh│\n│  ter spreads like a disease, and soon everyone │\n│  is laughing at you. You need to strike at the │\n│  source of this perverse merriment quickly to s│\n╰────────────────────────────────────────────────╯"
  },
  {
    "path": "viewport/testdata/TestSizing/view-50x15-softwrap-scrolled-plus-1.golden",
    "content": "╭────────────────────────────────────────────────╮\n│e from an awesome \"Hollow knight\" game (https://│\n│store.steampowered.com/app/367520/Hollow_Knight/│\n│).                                              │\n│Precept One: 'Always Win Your Battles'. Losing a│\n│ battle earns you nothing and teaches you nothin│\n│g. Win your battles, or don't engage in them at │\n│all!                                            │\n│                                                │\n│Precept Two: 'Never Let Them Laugh at You'. Fool│\n│s laugh at everything, even at their superiors. │\n│But beware, laughter isn't harmless! Laughter sp│\n│reads like a disease, and soon everyone is laugh│\n│ing at you. You need to strike at the source of │\n╰────────────────────────────────────────────────╯"
  },
  {
    "path": "viewport/testdata/TestSizing/view-50x15-softwrap-scrolled-plus-2.golden",
    "content": "╭────────────────────────────────────────────────╮\n│store.steampowered.com/app/367520/Hollow_Knight/│\n│).                                              │\n│Precept One: 'Always Win Your Battles'. Losing a│\n│ battle earns you nothing and teaches you nothin│\n│g. Win your battles, or don't engage in them at │\n│all!                                            │\n│                                                │\n│Precept Two: 'Never Let Them Laugh at You'. Fool│\n│s laugh at everything, even at their superiors. │\n│But beware, laughter isn't harmless! Laughter sp│\n│reads like a disease, and soon everyone is laugh│\n│ing at you. You need to strike at the source of │\n│this perverse merriment quickly to stop it from │\n╰────────────────────────────────────────────────╯"
  },
  {
    "path": "viewport/viewport.go",
    "content": "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/v2\"\n\t\"charm.land/lipgloss/v2\"\n\t\"github.com/charmbracelet/x/ansi\"\n)\n\nconst (\n\tdefaultHorizontalStep = 6\n)\n\n// Option is a configuration option that works in conjunction with [New]. For\n// example:\n//\n//\ttimer := New(WithWidth(10, WithHeight(5)))\ntype Option func(*Model)\n\n// WithWidth is an initialization option that sets the width of the\n// viewport. Pass as an argument to [New].\nfunc WithWidth(w int) Option {\n\treturn func(m *Model) {\n\t\tm.width = w\n\t}\n}\n\n// WithHeight is an initialization option that sets the height of the\n// viewport. Pass as an argument to [New].\nfunc WithHeight(h int) Option {\n\treturn func(m *Model) {\n\t\tm.height = h\n\t}\n}\n\n// New returns a new model with the given width and height as well as default\n// key mappings.\nfunc New(opts ...Option) (m Model) {\n\tfor _, opt := range opts {\n\t\topt(&m)\n\t}\n\tm.setInitialValues()\n\treturn m\n}\n\n// Model is the Bubble Tea model for this viewport element.\ntype Model struct {\n\twidth  int\n\theight int\n\tKeyMap KeyMap\n\n\t// Whether or not to wrap text. If false, it'll allow horizontal scrolling\n\t// instead.\n\tSoftWrap bool\n\n\t// Whether or not to fill to the height of the viewport with empty lines.\n\tFillHeight bool\n\n\t// Whether or not to respond to the mouse. The mouse must be enabled in\n\t// Bubble Tea for this to work. For details, see the Bubble Tea docs.\n\tMouseWheelEnabled bool\n\n\t// The number of lines the mouse wheel will scroll. By default, this is 3.\n\tMouseWheelDelta int\n\n\t// yOffset is the vertical scroll position.\n\tyOffset int\n\n\t// xOffset is the horizontal scroll position.\n\txOffset int\n\n\t// horizontalStep is the number of columns we move left or right during a\n\t// default horizontal scroll.\n\thorizontalStep int\n\n\t// YPosition is the position of the viewport in relation to the terminal\n\t// window. It's used in high performance rendering only.\n\tYPosition int\n\n\t// Style applies a lipgloss style to the viewport. Realistically, it's most\n\t// useful for setting borders, margins and padding.\n\tStyle lipgloss.Style\n\n\t// LeftGutterFunc allows to define a [GutterFunc] that adds a column into\n\t// the left of the viewport, which is kept when horizontal scrolling.\n\t// This can be used for things like line numbers, selection indicators,\n\t// show statuses, etc. It is expected that the real-width (as measured by\n\t// [lipgloss.Width]) of the returned value is always consistent, regardless\n\t// of index, soft wrapping, etc.\n\tLeftGutterFunc GutterFunc\n\n\tinitialized      bool\n\tlines            []string\n\tlongestLineWidth int\n\n\t// HighlightStyle highlights the ranges set with [SetHighligths].\n\tHighlightStyle lipgloss.Style\n\n\t// SelectedHighlightStyle highlights the highlight range focused during\n\t// navigation.\n\t// Use [SetHighligths] to set the highlight ranges, and [HightlightNext]\n\t// and [HihglightPrevious] to navigate.\n\tSelectedHighlightStyle lipgloss.Style\n\n\t// StyleLineFunc allows to return a [lipgloss.Style] for each line.\n\t// The argument is the line index.\n\tStyleLineFunc func(int) lipgloss.Style\n\n\thighlights []highlightInfo\n\thiIdx      int\n}\n\n// GutterFunc can be implemented and set into [Model.LeftGutterFunc].\n//\n// Example implementation showing line numbers:\n//\n//\tfunc(info GutterContext) string {\n//\t\tif info.Soft {\n//\t\t\treturn \"     │ \"\n//\t\t}\n//\t\tif info.Index >= info.TotalLines {\n//\t\t\treturn \"   ~ │ \"\n//\t\t}\n//\t\treturn fmt.Sprintf(\"%4d │ \", info.Index+1)\n//\t}\ntype GutterFunc func(GutterContext) string\n\n// NoGutter is the default gutter used.\nvar NoGutter = func(GutterContext) string { return \"\" }\n\n// GutterContext provides context to a [GutterFunc].\ntype GutterContext struct {\n\t// Index is the line index of the line which the gutter is being rendered for.\n\tIndex int\n\n\t// TotalLines is the total number of lines in the viewport.\n\tTotalLines int\n\n\t// Soft is whether or not the line is soft wrapped.\n\tSoft bool\n}\n\nfunc (m *Model) setInitialValues() {\n\tm.KeyMap = DefaultKeyMap()\n\tm.MouseWheelEnabled = true\n\tm.MouseWheelDelta = 3\n\tm.horizontalStep = defaultHorizontalStep\n\tm.LeftGutterFunc = NoGutter\n\tm.initialized = true\n}\n\n// Init exists to satisfy the tea.Model interface for composability purposes.\nfunc (m Model) Init() tea.Cmd {\n\treturn nil\n}\n\n// Height returns the height of the viewport.\nfunc (m Model) Height() int {\n\treturn m.height\n}\n\n// SetHeight sets the height of the viewport.\nfunc (m *Model) SetHeight(h int) {\n\tm.height = h\n}\n\n// Width returns the width of the viewport.\nfunc (m Model) Width() int {\n\treturn m.width\n}\n\n// SetWidth sets the width of the viewport.\nfunc (m *Model) SetWidth(w int) {\n\tm.width = w\n}\n\n// AtTop returns whether or not the viewport is at the very top position.\nfunc (m Model) AtTop() bool {\n\treturn m.YOffset() <= 0\n}\n\n// AtBottom returns whether or not the viewport is at or past the very bottom\n// position.\nfunc (m Model) AtBottom() bool {\n\treturn m.YOffset() >= m.maxYOffset()\n}\n\n// PastBottom returns whether or not the viewport is scrolled beyond the last\n// line. This can happen when adjusting the viewport height.\nfunc (m Model) PastBottom() bool {\n\treturn m.YOffset() > m.maxYOffset()\n}\n\n// ScrollPercent returns the amount scrolled as a float between 0 and 1.\nfunc (m Model) ScrollPercent() float64 {\n\ttotal, _, _ := m.calculateLine(0)\n\tif m.Height() >= total {\n\t\treturn 1.0\n\t}\n\ty := float64(m.YOffset())\n\th := float64(m.Height())\n\tt := float64(total)\n\tv := y / (t - h)\n\treturn clamp(v, 0, 1)\n}\n\n// HorizontalScrollPercent returns the amount horizontally scrolled as a float\n// between 0 and 1.\nfunc (m Model) HorizontalScrollPercent() float64 {\n\tif m.xOffset >= m.longestLineWidth-m.Width() {\n\t\treturn 1.0\n\t}\n\ty := float64(m.xOffset)\n\th := float64(m.Width())\n\tt := float64(m.longestLineWidth)\n\tv := y / (t - h)\n\treturn clamp(v, 0, 1)\n}\n\n// SetContent set the pager's text content. Line endings will be normalized to '\\n'.\nfunc (m *Model) SetContent(s string) {\n\tm.SetContentLines(strings.Split(s, \"\\n\"))\n}\n\n// SetContentLines allows to set the lines to be shown instead of the content.\n// If a given line has a \\n in it, it will still be split into multiple lines\n// similar to that of [Model.SetContent]. See also [Model.SetContent].\nfunc (m *Model) SetContentLines(lines []string) {\n\t// if there's no content, set content to actual nil instead of one empty\n\t// line.\n\tm.lines = lines\n\tif len(m.lines) == 1 && ansi.StringWidth(m.lines[0]) == 0 {\n\t\tm.lines = nil\n\t} else {\n\t\t// iterate in reverse, so we can safely modify the slice.\n\t\tvar subLines []string\n\t\tfor i := len(m.lines) - 1; i >= 0; i-- {\n\t\t\tif !strings.ContainsAny(m.lines[i], \"\\r\\n\") {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tm.lines[i] = strings.ReplaceAll(m.lines[i], \"\\r\\n\", \"\\n\") // normalize line endings\n\t\t\tsubLines = strings.Split(m.lines[i], \"\\n\")\n\t\t\tif len(subLines) > 1 {\n\t\t\t\tm.lines = slices.Insert(m.lines, i+1, subLines[1:]...)\n\t\t\t\tm.lines[i] = subLines[0]\n\t\t\t}\n\t\t}\n\t}\n\n\tm.longestLineWidth = maxLineWidth(m.lines)\n\tm.ClearHighlights()\n\n\tif m.YOffset() > m.maxYOffset() {\n\t\tm.GotoBottom()\n\t}\n}\n\n// GetContent returns the entire content as a single string.\n// Line endings are normalized to '\\n'.\nfunc (m Model) GetContent() string {\n\treturn strings.Join(m.lines, \"\\n\")\n}\n\n// calculateLine taking soft wrapping into account, returns the total viewable\n// lines and the real-line index for the given yoffset, as well as the virtual\n// line offset.\nfunc (m Model) calculateLine(yoffset int) (total, ridx, voffset int) {\n\tif !m.SoftWrap {\n\t\ttotal = len(m.lines)\n\t\tridx = min(yoffset, len(m.lines))\n\t\treturn total, ridx, 0\n\t}\n\n\tmaxWidth := float64(m.maxWidth())\n\tvar lineHeight int\n\n\tfor i, line := range m.lines {\n\t\tlineHeight = max(1, int(math.Ceil(float64(ansi.StringWidth(line))/maxWidth)))\n\n\t\tif yoffset >= total && yoffset < total+lineHeight {\n\t\t\tridx = i\n\t\t\tvoffset = yoffset - total\n\t\t}\n\t\ttotal += lineHeight\n\t}\n\n\tif yoffset >= total {\n\t\tridx = len(m.lines)\n\t\tvoffset = 0\n\t}\n\n\treturn total, ridx, voffset\n}\n\n// maxYOffset returns the maximum possible value of the y-offset based on the\n// viewport's content and set height.\nfunc (m Model) maxYOffset() int {\n\ttotal, _, _ := m.calculateLine(0)\n\treturn max(0, total-m.Height()+m.Style.GetVerticalFrameSize())\n}\n\n// maxXOffset returns the maximum possible value of the x-offset based on the\n// viewport's content and set width.\nfunc (m Model) maxXOffset() int {\n\treturn max(0, m.longestLineWidth-m.Width())\n}\n\n// maxWidth returns the maximum width of the viewport. It accounts for the frame\n// size, in addition to the gutter size.\nfunc (m Model) maxWidth() int {\n\tvar gutterSize int\n\tif m.LeftGutterFunc != nil {\n\t\tgutterSize = ansi.StringWidth(m.LeftGutterFunc(GutterContext{}))\n\t}\n\treturn max(0, m.Width()-m.Style.GetHorizontalFrameSize()-gutterSize)\n}\n\n// maxHeight returns the maximum height of the viewport. It accounts for the frame\n// size.\nfunc (m Model) maxHeight() int {\n\treturn max(0, m.Height()-m.Style.GetVerticalFrameSize())\n}\n\n// visibleLines returns the lines that should currently be visible in the\n// viewport.\nfunc (m Model) visibleLines() (lines []string) {\n\tmaxHeight := m.maxHeight()\n\tmaxWidth := m.maxWidth()\n\n\tif maxHeight == 0 || maxWidth == 0 {\n\t\treturn nil\n\t}\n\n\ttotal, ridx, voffset := m.calculateLine(m.YOffset())\n\tif total > 0 {\n\t\tbottom := clamp(ridx+maxHeight, ridx, len(m.lines))\n\t\tlines = m.styleLines(slices.Clone(m.lines[ridx:bottom]), ridx)\n\t\tlines = m.highlightLines(lines, ridx)\n\t}\n\n\tfor m.FillHeight && len(lines) < maxHeight {\n\t\tlines = append(lines, \"\")\n\t}\n\n\t// if longest line fit within width, no need to do anything else.\n\tif (m.xOffset == 0 && m.longestLineWidth <= maxWidth) || maxWidth == 0 {\n\t\treturn m.setupGutter(lines, total, ridx)\n\t}\n\n\tif m.SoftWrap {\n\t\treturn m.softWrap(lines, maxWidth, maxHeight, total, ridx, voffset)\n\t}\n\n\t// Cut the lines to the viewport width.\n\tfor i := range lines {\n\t\tlines[i] = ansi.Cut(lines[i], m.xOffset, m.xOffset+maxWidth)\n\t}\n\treturn m.setupGutter(lines, total, ridx)\n}\n\n// styleLines styles the lines using [Model.StyleLineFunc].\nfunc (m Model) styleLines(lines []string, offset int) []string {\n\tif m.StyleLineFunc == nil {\n\t\treturn lines\n\t}\n\tfor i := range lines {\n\t\tlines[i] = m.StyleLineFunc(i + offset).Render(lines[i])\n\t}\n\treturn lines\n}\n\n// highlightLines highlights the lines with [Model.HighlightStyle] and\n// [Model.SelectedHighlightStyle].\nfunc (m Model) highlightLines(lines []string, offset int) []string {\n\tif len(m.highlights) == 0 {\n\t\treturn lines\n\t}\n\tfor i := range lines {\n\t\tranges := makeHighlightRanges(\n\t\t\tm.highlights,\n\t\t\ti+offset,\n\t\t\tm.HighlightStyle,\n\t\t)\n\t\tlines[i] = lipgloss.StyleRanges(lines[i], ranges...)\n\t\tif m.hiIdx < 0 {\n\t\t\tcontinue\n\t\t}\n\t\tsel := m.highlights[m.hiIdx]\n\t\tif hi, ok := sel.lines[i+offset]; ok {\n\t\t\tlines[i] = lipgloss.StyleRanges(lines[i], lipgloss.NewRange(\n\t\t\t\thi[0],\n\t\t\t\thi[1],\n\t\t\t\tm.SelectedHighlightStyle,\n\t\t\t))\n\t\t}\n\t}\n\treturn lines\n}\n\nfunc (m Model) softWrap(lines []string, maxWidth, maxHeight, total, ridx, voffset int) []string {\n\twrappedLines := make([]string, 0, maxHeight)\n\n\tvar idx, lineWidth int\n\tvar truncatedLine string\n\n\tfor i, line := range lines {\n\t\t// If the line is less than or equal to the max width, it can be added\n\t\t// as is.\n\t\tlineWidth = ansi.StringWidth(line)\n\n\t\tif lineWidth <= maxWidth {\n\t\t\tif m.LeftGutterFunc != nil {\n\t\t\t\tline = m.LeftGutterFunc(GutterContext{\n\t\t\t\t\tIndex:      i + ridx,\n\t\t\t\t\tTotalLines: total,\n\t\t\t\t\tSoft:       false,\n\t\t\t\t}) + line\n\t\t\t}\n\t\t\twrappedLines = append(wrappedLines, line)\n\t\t\tcontinue\n\t\t}\n\n\t\tidx = 0\n\t\tfor lineWidth > idx {\n\t\t\ttruncatedLine = ansi.Cut(line, idx, maxWidth+idx)\n\t\t\tif m.LeftGutterFunc != nil {\n\t\t\t\ttruncatedLine = m.LeftGutterFunc(GutterContext{\n\t\t\t\t\tIndex:      i + ridx,\n\t\t\t\t\tTotalLines: total,\n\t\t\t\t\tSoft:       idx > 0,\n\t\t\t\t}) + truncatedLine\n\t\t\t}\n\t\t\twrappedLines = append(wrappedLines, truncatedLine)\n\t\t\tidx += maxWidth\n\t\t}\n\t}\n\n\treturn wrappedLines[voffset:min(voffset+maxHeight, len(wrappedLines))]\n}\n\n// setupGutter sets up the left gutter using [Model.LeftGutterFunc].\nfunc (m Model) setupGutter(lines []string, total, ridx int) []string {\n\tif m.LeftGutterFunc == nil {\n\t\treturn lines\n\t}\n\n\tfor i := range lines {\n\t\tlines[i] = m.LeftGutterFunc(GutterContext{\n\t\t\tIndex:      i + ridx,\n\t\t\tTotalLines: total,\n\t\t\tSoft:       false,\n\t\t}) + lines[i]\n\t}\n\treturn lines\n}\n\n// SetYOffset sets the Y offset.\nfunc (m *Model) SetYOffset(n int) {\n\tm.yOffset = clamp(n, 0, m.maxYOffset())\n}\n\n// YOffset returns the current Y offset - the vertical scroll position.\nfunc (m *Model) YOffset() int { return m.yOffset }\n\n// EnsureVisible ensures that the given line and column are in the viewport.\nfunc (m *Model) EnsureVisible(line, colstart, colend int) {\n\tmaxWidth := m.maxWidth()\n\tif colend <= maxWidth {\n\t\tm.SetXOffset(0)\n\t} else {\n\t\tm.SetXOffset(colstart - m.horizontalStep) // put one step to the left, feels more natural\n\t}\n\n\tif line < m.YOffset() || line >= m.YOffset()+m.maxHeight() {\n\t\tm.SetYOffset(line)\n\t}\n}\n\n// PageDown moves the view down by the number of lines in the viewport.\nfunc (m *Model) PageDown() {\n\tif m.AtBottom() {\n\t\treturn\n\t}\n\tm.ScrollDown(m.Height())\n}\n\n// PageUp moves the view up by one height of the viewport.\nfunc (m *Model) PageUp() {\n\tif m.AtTop() {\n\t\treturn\n\t}\n\tm.ScrollUp(m.Height())\n}\n\n// HalfPageDown moves the view down by half the height of the viewport.\nfunc (m *Model) HalfPageDown() {\n\tif m.AtBottom() {\n\t\treturn\n\t}\n\tm.ScrollDown(m.Height() / 2) //nolint:mnd\n}\n\n// HalfPageUp moves the view up by half the height of the viewport.\nfunc (m *Model) HalfPageUp() {\n\tif m.AtTop() {\n\t\treturn\n\t}\n\tm.ScrollUp(m.Height() / 2) //nolint:mnd\n}\n\n// ScrollDown moves the view down by the given number of lines.\nfunc (m *Model) ScrollDown(n int) {\n\tif m.AtBottom() || n == 0 || len(m.lines) == 0 {\n\t\treturn\n\t}\n\t// Make sure the number of lines by which we're going to scroll isn't\n\t// greater than the number of lines we actually have left before we reach\n\t// the bottom.\n\tm.SetYOffset(m.YOffset() + n)\n\tm.hiIdx = m.findNearestMatch()\n}\n\n// ScrollUp moves the view up by the given number of lines.\nfunc (m *Model) ScrollUp(n int) {\n\tif m.AtTop() || n == 0 || len(m.lines) == 0 {\n\t\treturn\n\t}\n\t// Make sure the number of lines by which we're going to scroll isn't\n\t// greater than the number of lines we are from the top.\n\tm.SetYOffset(m.YOffset() - n)\n\tm.hiIdx = m.findNearestMatch()\n}\n\n// SetHorizontalStep sets the amount of cells that the viewport moves in the\n// default viewport keymapping. If set to 0 or less, horizontal scrolling is\n// disabled.\nfunc (m *Model) SetHorizontalStep(n int) {\n\tm.horizontalStep = max(0, n)\n}\n\n// XOffset returns the current X offset - the horizontal scroll position.\nfunc (m *Model) XOffset() int { return m.xOffset }\n\n// SetXOffset sets the X offset.\n// No-op when soft wrap is enabled.\nfunc (m *Model) SetXOffset(n int) {\n\tif m.SoftWrap {\n\t\treturn\n\t}\n\tm.xOffset = clamp(n, 0, m.maxXOffset())\n}\n\n// ScrollLeft moves the viewport to the left by the given number of columns.\nfunc (m *Model) ScrollLeft(n int) {\n\tm.SetXOffset(m.xOffset - n)\n}\n\n// ScrollRight moves viewport to the right by the given number of columns.\nfunc (m *Model) ScrollRight(n int) {\n\tm.SetXOffset(m.xOffset + n)\n}\n\n// TotalLineCount returns the total number of lines (both hidden and visible) within the viewport.\nfunc (m Model) TotalLineCount() int {\n\ttotal, _, _ := m.calculateLine(0)\n\treturn total\n}\n\n// VisibleLineCount returns the number of the visible lines within the viewport.\nfunc (m Model) VisibleLineCount() int {\n\treturn len(m.visibleLines())\n}\n\n// GotoTop sets the viewport to the top position.\nfunc (m *Model) GotoTop() (lines []string) {\n\tif m.AtTop() {\n\t\treturn nil\n\t}\n\tm.SetYOffset(0)\n\tm.hiIdx = m.findNearestMatch()\n\treturn m.visibleLines()\n}\n\n// GotoBottom sets the viewport to the bottom position.\nfunc (m *Model) GotoBottom() (lines []string) {\n\tm.SetYOffset(m.maxYOffset())\n\tm.hiIdx = m.findNearestMatch()\n\treturn m.visibleLines()\n}\n\n// SetHighlights sets ranges of characters to highlight.\n// For instance, `[]int{[]int{2, 10}, []int{20, 30}}` will highlight characters\n// 2 to 10 and 20 to 30.\n// Note that highlights are not expected to transpose each other, and are also\n// expected to be in order.\n// Use [Model.SetHighlights] to set the highlight ranges, and\n// [Model.HighlightNext] and [Model.HighlightPrevious] to navigate.\n// Use [Model.ClearHighlights] to remove all highlights.\nfunc (m *Model) SetHighlights(matches [][]int) {\n\tif len(matches) == 0 || len(m.lines) == 0 {\n\t\treturn\n\t}\n\tm.highlights = parseMatches(m.GetContent(), matches)\n\tm.hiIdx = m.findNearestMatch()\n\tm.showHighlight()\n}\n\n// ClearHighlights clears previously set highlights.\nfunc (m *Model) ClearHighlights() {\n\tm.highlights = nil\n\tm.hiIdx = -1\n}\n\nfunc (m *Model) showHighlight() {\n\tif m.hiIdx == -1 {\n\t\treturn\n\t}\n\tline, colstart, colend := m.highlights[m.hiIdx].coords()\n\tm.EnsureVisible(line, colstart, colend)\n}\n\n// HighlightNext highlights the next match.\nfunc (m *Model) HighlightNext() {\n\tif m.highlights == nil {\n\t\treturn\n\t}\n\tm.hiIdx = (m.hiIdx + 1) % len(m.highlights)\n\tm.showHighlight()\n}\n\n// HighlightPrevious highlights the previous match.\nfunc (m *Model) HighlightPrevious() {\n\tif m.highlights == nil {\n\t\treturn\n\t}\n\tm.hiIdx = (m.hiIdx - 1 + len(m.highlights)) % len(m.highlights)\n\tm.showHighlight()\n}\n\nfunc (m Model) findNearestMatch() int {\n\tfor i, match := range m.highlights {\n\t\tif match.lineStart >= m.YOffset() {\n\t\t\treturn i\n\t\t}\n\t}\n\treturn -1\n}\n\n// Update handles standard message-based viewport updates.\nfunc (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {\n\tm = m.updateAsModel(msg)\n\treturn m, nil\n}\n\n// Author's note: this method has been broken out to make it easier to\n// potentially transition Update to satisfy tea.Model.\nfunc (m Model) updateAsModel(msg tea.Msg) Model {\n\tif !m.initialized {\n\t\tm.setInitialValues()\n\t}\n\n\tswitch msg := msg.(type) {\n\tcase tea.KeyPressMsg:\n\t\tswitch {\n\t\tcase key.Matches(msg, m.KeyMap.PageDown):\n\t\t\tm.PageDown()\n\n\t\tcase key.Matches(msg, m.KeyMap.PageUp):\n\t\t\tm.PageUp()\n\n\t\tcase key.Matches(msg, m.KeyMap.HalfPageDown):\n\t\t\tm.HalfPageDown()\n\n\t\tcase key.Matches(msg, m.KeyMap.HalfPageUp):\n\t\t\tm.HalfPageUp()\n\n\t\tcase key.Matches(msg, m.KeyMap.Down):\n\t\t\tm.ScrollDown(1)\n\n\t\tcase key.Matches(msg, m.KeyMap.Up):\n\t\t\tm.ScrollUp(1)\n\n\t\tcase key.Matches(msg, m.KeyMap.Left):\n\t\t\tm.ScrollLeft(m.horizontalStep)\n\n\t\tcase key.Matches(msg, m.KeyMap.Right):\n\t\t\tm.ScrollRight(m.horizontalStep)\n\t\t}\n\n\tcase tea.MouseWheelMsg:\n\t\tif !m.MouseWheelEnabled {\n\t\t\tbreak\n\t\t}\n\t\tswitch msg.Button {\n\t\tcase tea.MouseWheelDown:\n\t\t\t// NOTE: some terminal emulators don't send the shift event for\n\t\t\t// mouse actions.\n\t\t\tif msg.Mod.Contains(tea.ModShift) {\n\t\t\t\tm.ScrollRight(m.horizontalStep)\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tm.ScrollDown(m.MouseWheelDelta)\n\t\tcase tea.MouseWheelUp:\n\t\t\t// NOTE: some terminal emulators don't send the shift event for\n\t\t\t// mouse actions.\n\t\t\tif msg.Mod.Contains(tea.ModShift) {\n\t\t\t\tm.ScrollLeft(m.horizontalStep)\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tm.ScrollUp(m.MouseWheelDelta)\n\t\tcase tea.MouseWheelLeft:\n\t\t\tm.ScrollLeft(m.horizontalStep)\n\t\tcase tea.MouseWheelRight:\n\t\t\tm.ScrollRight(m.horizontalStep)\n\t\t}\n\t}\n\n\treturn m\n}\n\n// View renders the viewport into a string.\nfunc (m Model) View() string {\n\tw, h := m.Width(), m.Height()\n\tif sw := m.Style.GetWidth(); sw != 0 {\n\t\tw = min(w, sw)\n\t}\n\tif sh := m.Style.GetHeight(); sh != 0 {\n\t\th = min(h, sh)\n\t}\n\n\tif w == 0 || h == 0 {\n\t\treturn \"\"\n\t}\n\n\tcontentWidth := w - m.Style.GetHorizontalFrameSize()\n\tcontentHeight := h - m.Style.GetVerticalFrameSize()\n\tcontents := lipgloss.NewStyle().\n\t\tWidth(contentWidth).   // pad to width.\n\t\tHeight(contentHeight). // pad to height.\n\t\tRender(strings.Join(m.visibleLines(), \"\\n\"))\n\treturn m.Style.\n\t\tUnsetWidth().UnsetHeight(). // Style size already applied in contents.\n\t\tRender(contents)\n}\n\nfunc clamp[T cmp.Ordered](v, low, high T) T {\n\tif high < low {\n\t\tlow, high = high, low\n\t}\n\treturn min(high, max(low, v))\n}\n\nfunc maxLineWidth(lines []string) int {\n\tresult := 0\n\tfor _, line := range lines {\n\t\tresult = max(result, ansi.StringWidth(line))\n\t}\n\treturn result\n}\n"
  },
  {
    "path": "viewport/viewport_test.go",
    "content": "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/charmbracelet/x/ansi\"\n\t\"github.com/charmbracelet/x/exp/golden\"\n)\n\ntype suffixedTest struct {\n\ttesting.TB\n\tsuffix string\n}\n\nfunc (s *suffixedTest) Name() string {\n\treturn fmt.Sprintf(\"%s-%s\", s.TB.Name(), s.suffix)\n}\n\n// withSuffix is a helper to add a temporary suffix to the test name. Primarily\n// useful for golden tests since there is currently no way to have multiple snapshots\n// in the same test.\nfunc withSuffix(t testing.TB, suffix string) testing.TB {\n\tt.Helper()\n\n\treturn &suffixedTest{TB: t, suffix: suffix}\n}\n\nconst textContentList = `57 Precepts of narcissistic comedy character Zote from an awesome \"Hollow knight\" game (https://store.steampowered.com/app/367520/Hollow_Knight/).\nPrecept One: 'Always Win Your Battles'. Losing a battle earns you nothing and teaches you nothing. Win your battles, or don't engage in them at all!\n\nPrecept Two: 'Never Let Them Laugh at You'. Fools laugh at everything, even at their superiors. But beware, laughter isn't harmless! Laughter spreads like a disease, and soon everyone is laughing at you. You need to strike at the source of this perverse merriment quickly to stop it from spreading.\nPrecept Three: 'Always Be Rested'. Fighting and adventuring take their toll on your body. When you rest, your body strengthens and repairs itself. The longer you rest, the stronger you become.\nPrecept Four: 'Forget Your Past'. The past is painful, and thinking about your past can only bring you misery. Think about something else instead, such as the future, or some food.\nPrecept Five: 'Strength Beats Strength'. Is your opponent strong? No matter! Simply overcome their strength with even more strength, and they'll soon be defeated.\nPrecept Six: 'Choose Your Own Fate'. Our elders teach that our fate is chosen for us before we are even born. I disagree.\nPrecept Seven: 'Mourn Not the Dead'. When we die, do things get better for us or worse? There's no way to tell, so we shouldn't bother mourning. Or celebrating for that matter.\nPrecept Eight: 'Travel Alone'. You can rely on nobody, and nobody will always be loyal. Therefore, nobody should be your constant companion.\nPrecept Nine: 'Keep Your Home Tidy'. Your home is where you keep your most prized possession - yourself. Therefore, you should make an effort to keep it nice and clean.\nPrecept Ten: 'Keep Your Weapon Sharp'. I make sure that my weapon, 'Life Ender', is kept well-sharpened at all times. This makes it much easier to cut things.\nPrecept Eleven: 'Mothers Will Always Betray You'. This Precept explains itself.\nPrecept Twelve: 'Keep Your Cloak Dry'. If your cloak gets wet, dry it as soon as you can. Wearing wet cloaks is unpleasant, and can lead to illness.\nPrecept Thirteen: 'Never Be Afraid'. Fear can only hold you back. Facing your fears can be a tremendous effort. Therefore, you should just not be afraid in the first place.\nPrecept Fourteen: 'Respect Your Superiors'. If someone is your superior in strength or intellect or both, you need to show them your respect. Don't ignore them or laugh at them.\nPrecept Fifteen: 'One Foe, One Blow'. You should only use a single blow to defeat an enemy. Any more is a waste. Also, by counting your blows as you fight, you'll know how many foes you've defeated.`\n\nfunc TestNew(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"default values on create by New\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tm := New(WithHeight(10), WithWidth(10))\n\n\t\tif !m.initialized {\n\t\t\tt.Errorf(\"on create by New, Model should be initialized\")\n\t\t}\n\n\t\tif m.horizontalStep != defaultHorizontalStep {\n\t\t\tt.Errorf(\"default horizontalStep should be %d, got %d\", defaultHorizontalStep, m.horizontalStep)\n\t\t}\n\n\t\tif m.MouseWheelDelta != 3 {\n\t\t\tt.Errorf(\"default MouseWheelDelta should be 3, got %d\", m.MouseWheelDelta)\n\t\t}\n\n\t\tif !m.MouseWheelEnabled {\n\t\t\tt.Error(\"mouse wheel should be enabled by default\")\n\t\t}\n\t})\n}\n\nfunc TestSetInitialValues(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"default horizontalStep\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tm := Model{}\n\t\tm.setInitialValues()\n\n\t\tif m.horizontalStep != defaultHorizontalStep {\n\t\t\tt.Errorf(\"default horizontalStep should be %d, got %d\", defaultHorizontalStep, m.horizontalStep)\n\t\t}\n\t})\n}\n\nfunc TestSetHorizontalStep(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"change default\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tm := New(WithHeight(10), WithWidth(10))\n\n\t\tif m.horizontalStep != defaultHorizontalStep {\n\t\t\tt.Errorf(\"default horizontalStep should be %d, got %d\", defaultHorizontalStep, m.horizontalStep)\n\t\t}\n\n\t\tnewStep := 8\n\t\tm.SetHorizontalStep(newStep)\n\t\tif m.horizontalStep != newStep {\n\t\t\tt.Errorf(\"horizontalStep should be %d, got %d\", newStep, m.horizontalStep)\n\t\t}\n\t})\n\n\tt.Run(\"no negative\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tm := New(WithHeight(10), WithWidth(10))\n\n\t\tif m.horizontalStep != defaultHorizontalStep {\n\t\t\tt.Errorf(\"default horizontalStep should be %d, got %d\", defaultHorizontalStep, m.horizontalStep)\n\t\t}\n\n\t\tzero := 0\n\t\tm.SetHorizontalStep(-1)\n\t\tif m.horizontalStep != zero {\n\t\t\tt.Errorf(\"horizontalStep should be %d, got %d\", zero, m.horizontalStep)\n\t\t}\n\t})\n}\n\nfunc TestMoveLeft(t *testing.T) {\n\tt.Parallel()\n\n\tzeroPosition := 0\n\n\tt.Run(\"zero position\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tm := New(WithHeight(10), WithWidth(10))\n\t\tif m.xOffset != zeroPosition {\n\t\t\tt.Errorf(\"default indent should be %d, got %d\", zeroPosition, m.xOffset)\n\t\t}\n\n\t\tm.ScrollLeft(m.horizontalStep)\n\t\tif m.xOffset != zeroPosition {\n\t\t\tt.Errorf(\"indent should be %d, got %d\", zeroPosition, m.xOffset)\n\t\t}\n\t})\n\n\tt.Run(\"move\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tm := New(WithHeight(10), WithWidth(10))\n\t\tm.longestLineWidth = 100\n\t\tif m.xOffset != zeroPosition {\n\t\t\tt.Errorf(\"default indent should be %d, got %d\", zeroPosition, m.xOffset)\n\t\t}\n\n\t\tm.xOffset = defaultHorizontalStep * 2\n\t\tm.ScrollLeft(m.horizontalStep)\n\t\tnewIndent := defaultHorizontalStep\n\t\tif m.xOffset != newIndent {\n\t\t\tt.Errorf(\"indent should be %d, got %d\", newIndent, m.xOffset)\n\t\t}\n\t})\n}\n\nfunc TestMoveRight(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"move\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tzeroPosition := 0\n\n\t\tm := New(WithHeight(10), WithWidth(10))\n\t\tm.SetContent(\"Some line that is longer than width\")\n\t\tif m.xOffset != zeroPosition {\n\t\t\tt.Errorf(\"default indent should be %d, got %d\", zeroPosition, m.xOffset)\n\t\t}\n\n\t\tm.ScrollRight(m.horizontalStep)\n\t\tnewIndent := defaultHorizontalStep\n\t\tif m.xOffset != newIndent {\n\t\t\tt.Errorf(\"indent should be %d, got %d\", newIndent, m.xOffset)\n\t\t}\n\t})\n}\n\nfunc TestResetIndent(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"reset\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tzeroPosition := 0\n\n\t\tm := New(WithHeight(10), WithWidth(10))\n\t\tm.xOffset = 500\n\n\t\tm.SetXOffset(0)\n\t\tif m.xOffset != zeroPosition {\n\t\t\tt.Errorf(\"indent should be %d, got %d\", zeroPosition, m.xOffset)\n\t\t}\n\t})\n}\n\nfunc TestVisibleLines(t *testing.T) {\n\tt.Parallel()\n\n\tdefaultList := strings.Split(textContentList, \"\\n\")\n\n\tt.Run(\"empty list\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tm := New(WithHeight(10), WithWidth(10))\n\t\tlist := m.visibleLines()\n\n\t\tif len(list) != 0 {\n\t\t\tt.Errorf(\"list should be empty, got %d\", len(list))\n\t\t}\n\t})\n\n\tt.Run(\"empty list: with indent\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tm := New(WithHeight(10), WithWidth(10))\n\t\tlist := m.visibleLines()\n\t\tm.xOffset = 5\n\n\t\tif len(list) != 0 {\n\t\t\tt.Errorf(\"list should be empty, got %d\", len(list))\n\t\t}\n\t})\n\n\tt.Run(\"list\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tnumberOfLines := 10\n\n\t\tm := New(WithHeight(numberOfLines), WithWidth(10))\n\t\tm.SetContent(strings.Join(defaultList, \"\\n\"))\n\n\t\tlist := m.visibleLines()\n\t\tif len(list) != numberOfLines {\n\t\t\tt.Errorf(\"list should have %d lines, got %d\", numberOfLines, len(list))\n\t\t}\n\n\t\tlastItemIdx := numberOfLines - 1\n\t\t// we trim line if it doesn't fit to width of the viewport\n\t\tshouldGet := defaultList[lastItemIdx][:m.Width()]\n\t\tif list[lastItemIdx] != shouldGet {\n\t\t\tt.Errorf(`%dth list item should be '%s', got '%s'`, lastItemIdx, shouldGet, list[lastItemIdx])\n\t\t}\n\t})\n\n\tt.Run(\"list: with y offset\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tnumberOfLines := 10\n\n\t\tm := New(WithHeight(numberOfLines), WithWidth(10))\n\t\tm.SetContent(strings.Join(defaultList, \"\\n\"))\n\t\tm.SetYOffset(5)\n\n\t\tlist := m.visibleLines()\n\t\tif len(list) != numberOfLines {\n\t\t\tt.Errorf(\"list should have %d lines, got %d\", numberOfLines, len(list))\n\t\t}\n\n\t\tif list[0] == defaultList[0] {\n\t\t\tt.Error(\"first item of list should not be the first item of initial list because of Y offset\")\n\t\t}\n\n\t\tlastItemIdx := numberOfLines - 1\n\t\t// we trim line if it doesn't fit to width of the viewport\n\t\tshouldGet := defaultList[m.YOffset()+lastItemIdx][:m.Width()]\n\t\tif list[lastItemIdx] != shouldGet {\n\t\t\tt.Errorf(`%dth list item should be '%s', got '%s'`, lastItemIdx, shouldGet, list[lastItemIdx])\n\t\t}\n\t})\n\n\tt.Run(\"list: with y offset: horizontal scroll\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tnumberOfLines := 10\n\n\t\tm := New(WithHeight(numberOfLines), WithWidth(10))\n\t\tm.lines = defaultList\n\t\tm.SetYOffset(7)\n\n\t\t// default list\n\t\tlist := m.visibleLines()\n\t\tif len(list) != numberOfLines {\n\t\t\tt.Errorf(\"list should have %d lines, got %d\", numberOfLines, len(list))\n\t\t}\n\n\t\tlastItem := numberOfLines - 1\n\t\tdefaultLastItem := len(defaultList) - 1\n\t\tif list[lastItem] != defaultList[defaultLastItem] {\n\t\t\tt.Errorf(\"%dth list item should the the same as %dth default list item\", lastItem, defaultLastItem)\n\t\t}\n\n\t\tperceptPrefix := \"Precept\"\n\t\tif !strings.HasPrefix(list[0], perceptPrefix) {\n\t\t\tt.Errorf(\"first list item has to have prefix %s\", perceptPrefix)\n\t\t}\n\n\t\t// move right\n\t\tm.ScrollRight(m.horizontalStep)\n\t\tlist = m.visibleLines()\n\n\t\tnewPrefix := perceptPrefix[m.xOffset:]\n\t\tif !strings.HasPrefix(list[0], newPrefix) {\n\t\t\tt.Errorf(\"first list item has to have prefix %s, get %s\", newPrefix, list[0])\n\t\t}\n\n\t\tif list[lastItem] != defaultList[defaultLastItem] {\n\t\t\tt.Errorf(\"last item should be empty, got %s\", list[lastItem])\n\t\t}\n\n\t\t// move left\n\t\tm.ScrollLeft(m.horizontalStep)\n\t\tlist = m.visibleLines()\n\t\tif !strings.HasPrefix(list[0], perceptPrefix) {\n\t\t\tt.Errorf(\"first list item has to have prefix %s\", perceptPrefix)\n\t\t}\n\n\t\tif list[lastItem] != defaultList[defaultLastItem] {\n\t\t\tt.Errorf(\"%dth list item should the the same as %dth default list item\", lastItem, defaultLastItem)\n\t\t}\n\t})\n\n\tt.Run(\"list: with 2 cells symbols: horizontal scroll\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tconst horizontalStep = 5\n\n\t\tinitList := []string{\n\t\t\t\"あいうえお\",\n\t\t\t\"Aあいうえお\",\n\t\t\t\"あいうえお\",\n\t\t\t\"Aあいうえお\",\n\t\t}\n\t\tnumberOfLines := len(initList)\n\n\t\tm := New(WithHeight(numberOfLines), WithWidth(20))\n\t\tm.lines = initList\n\t\tm.longestLineWidth = 30 // dirty hack: not checking right overscroll for this test case\n\n\t\t// default list\n\t\tlist := m.visibleLines()\n\t\tif len(list) != numberOfLines {\n\t\t\tt.Errorf(\"list should have %d lines, got %d\", numberOfLines, len(list))\n\t\t}\n\n\t\tlastItemIdx := numberOfLines - 1\n\t\tinitLastItem := len(initList) - 1\n\t\tshouldGet := initList[initLastItem]\n\t\tif list[lastItemIdx] != shouldGet {\n\t\t\tt.Errorf(\"%dth list item should the the same as %dth default list item\", lastItemIdx, initLastItem)\n\t\t}\n\n\t\t// move right\n\t\tm.ScrollRight(horizontalStep)\n\t\tlist = m.visibleLines()\n\n\t\tfor i := range list {\n\t\t\tcutLine := \"うえお\"\n\t\t\tif list[i] != cutLine {\n\t\t\t\tt.Errorf(\"line must be `%s`, get `%s`\", cutLine, list[i])\n\t\t\t}\n\t\t}\n\n\t\t// move left\n\t\tm.ScrollLeft(horizontalStep)\n\t\tlist = m.visibleLines()\n\t\tfor i := range list {\n\t\t\tif list[i] != initList[i] {\n\t\t\t\tt.Errorf(\"line must be `%s`, get `%s`\", list[i], initList[i])\n\t\t\t}\n\t\t}\n\n\t\t// move left second times do not change lites if indent == 0\n\t\tm.xOffset = 0\n\t\tm.ScrollLeft(horizontalStep)\n\t\tlist = m.visibleLines()\n\t\tfor i := range list {\n\t\t\tif list[i] != initList[i] {\n\t\t\t\tt.Errorf(\"line must be `%s`, get `%s`\", list[i], initList[i])\n\t\t\t}\n\t\t}\n\t})\n}\n\nfunc TestRightOverscroll(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"prevent right overscroll\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tcontent := \"Content is short\"\n\t\tm := New(WithHeight(5), WithWidth(len(content)+1))\n\t\tm.SetContent(content)\n\n\t\tfor range 10 {\n\t\t\tm.ScrollRight(m.horizontalStep)\n\t\t}\n\n\t\tvisibleLines := m.visibleLines()\n\t\tvisibleLine := visibleLines[0]\n\n\t\tif visibleLine != content {\n\t\t\tt.Error(\"visible line should stay the same as content\")\n\t\t}\n\t})\n}\n\nfunc TestMatchesToHighlights(t *testing.T) {\n\tcontent := `hello\nworld\n\nwith empty rows\n\nwide chars: あいうえおafter\n\n爱开源 • Charm does open source\n\nCharm热爱开源 • Charm loves open source\n`\n\n\tvt := New(WithWidth(100), WithHeight(100))\n\tvt.SetContent(content)\n\n\tt.Run(\"first\", func(t *testing.T) {\n\t\ttestHighlights(t, content, regexp.MustCompile(\"hello\"), []highlightInfo{\n\t\t\t{\n\t\t\t\tlineStart: 0,\n\t\t\t\tlineEnd:   0,\n\t\t\t\tlines: map[int][2]int{\n\t\t\t\t\t0: {0, 5},\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\t})\n\n\tt.Run(\"multiple\", func(t *testing.T) {\n\t\ttestHighlights(t, content, regexp.MustCompile(\"l\"), []highlightInfo{\n\t\t\t{\n\t\t\t\tlineStart: 0,\n\t\t\t\tlineEnd:   0,\n\t\t\t\tlines: map[int][2]int{\n\t\t\t\t\t0: {2, 3},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tlineStart: 0,\n\t\t\t\tlineEnd:   0,\n\t\t\t\tlines: map[int][2]int{\n\t\t\t\t\t0: {3, 4},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tlineStart: 1,\n\t\t\t\tlineEnd:   1,\n\t\t\t\tlines: map[int][2]int{\n\t\t\t\t\t1: {3, 4},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tlineStart: 9,\n\t\t\t\tlineEnd:   9,\n\t\t\t\tlines: map[int][2]int{\n\t\t\t\t\t9: {22, 23},\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\t})\n\n\tt.Run(\"span lines\", func(t *testing.T) {\n\t\ttestHighlights(t, content, regexp.MustCompile(\"lo\\nwo\"), []highlightInfo{\n\t\t\t{\n\t\t\t\tlineStart: 0,\n\t\t\t\tlineEnd:   1,\n\t\t\t\tlines: map[int][2]int{\n\t\t\t\t\t0: {3, 6},\n\t\t\t\t\t1: {0, 2},\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\t})\n\n\tt.Run(\"ends with newline\", func(t *testing.T) {\n\t\ttestHighlights(t, content, regexp.MustCompile(\"lo\\n\"), []highlightInfo{\n\t\t\t{\n\t\t\t\tlineStart: 0,\n\t\t\t\tlineEnd:   0,\n\t\t\t\tlines: map[int][2]int{\n\t\t\t\t\t0: {3, 6},\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\t})\n\n\tt.Run(\"empty lines in the text\", func(t *testing.T) {\n\t\ttestHighlights(t, content, regexp.MustCompile(\"ith\"), []highlightInfo{\n\t\t\t{\n\t\t\t\tlineStart: 3,\n\t\t\t\tlineEnd:   3,\n\t\t\t\tlines: map[int][2]int{\n\t\t\t\t\t3: {1, 4},\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\t})\n\n\tt.Run(\"empty lines in the text match start of new line\", func(t *testing.T) {\n\t\ttestHighlights(t, content, regexp.MustCompile(\"with\"), []highlightInfo{\n\t\t\t{\n\t\t\t\tlineStart: 3,\n\t\t\t\tlineEnd:   3,\n\t\t\t\tlines: map[int][2]int{\n\t\t\t\t\t3: {0, 4},\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\t})\n\n\tt.Run(\"wide characteres\", func(t *testing.T) {\n\t\ttestHighlights(t, content, regexp.MustCompile(\"after\"), []highlightInfo{\n\t\t\t{\n\t\t\t\tlineStart: 5,\n\t\t\t\tlineEnd:   5,\n\t\t\t\tlines: map[int][2]int{\n\t\t\t\t\t5: {22, 27},\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\t})\n\n\tt.Run(\"wide 2\", func(t *testing.T) {\n\t\ttestHighlights(t, content, regexp.MustCompile(\"Charm\"), []highlightInfo{\n\t\t\t{\n\t\t\t\tlineStart: 7,\n\t\t\t\tlineEnd:   7,\n\t\t\t\tlines: map[int][2]int{\n\t\t\t\t\t7: {9, 14},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tlineStart: 9,\n\t\t\t\tlineEnd:   9,\n\t\t\t\tlines: map[int][2]int{\n\t\t\t\t\t9: {0, 5},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tlineStart: 9,\n\t\t\t\tlineEnd:   9,\n\t\t\t\tlines: map[int][2]int{\n\t\t\t\t\t9: {16, 21},\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\t})\n}\n\nfunc testHighlights(tb testing.TB, content string, re *regexp.Regexp, expect []highlightInfo) {\n\ttb.Helper()\n\n\tvt := New(WithHeight(100), WithWidth(100))\n\tvt.SetContent(content)\n\n\tmatches := re.FindAllStringIndex(vt.GetContent(), -1)\n\tvt.SetHighlights(matches)\n\n\tif !reflect.DeepEqual(expect, vt.highlights) {\n\t\ttb.Errorf(\"\\nexpect: %+v\\n   got: %+v\\n\", expect, vt.highlights)\n\t}\n\n\tif strings.Contains(re.String(), \"\\n\") {\n\t\ttb.Log(\"cannot check text when regex has span lines\")\n\t\treturn\n\t}\n\n\tfor _, hi := range expect {\n\t\tfor line, hl := range hi.lines {\n\t\t\tcut := ansi.Cut(vt.lines[line], hl[0], hl[1])\n\t\t\tif !re.MatchString(cut) {\n\t\t\t\ttb.Errorf(\"exptect to match '%s', got '%s': line: %d, cut: %+v\", re.String(), cut, line, hl)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc TestSizing(t *testing.T) {\n\tt.Parallel()\n\n\tlines := strings.Split(textContentList, \"\\n\")\n\n\tt.Run(\"view-40x100percent\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\twidth := 40\n\t\theight := len(lines) + 2 // +2 for border.\n\n\t\tvt := New(WithWidth(width), WithHeight(height))\n\t\tvt.Style = vt.Style.Border(lipgloss.RoundedBorder())\n\t\tvt.SetContent(textContentList)\n\n\t\tview := vt.View()\n\t\tif w, h := lipgloss.Size(view); w != width || h != height {\n\t\t\tt.Errorf(\"view size should be %d x %d, got %d x %d\", width, height, w, h)\n\t\t}\n\n\t\tgolden.RequireEqual(t, view)\n\t})\n\n\tt.Run(\"view-50x15-softwrap\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\twidth := 50\n\t\theight := 15\n\n\t\tvt := New(WithWidth(width), WithHeight(height))\n\t\tvt.SoftWrap = true\n\t\tvt.Style = vt.Style.Border(lipgloss.RoundedBorder())\n\t\tvt.SetContent(textContentList)\n\n\t\tview := vt.View()\n\t\tif w, h := lipgloss.Size(view); w != width || h != height {\n\t\t\tt.Errorf(\"view size should be %d x %d, got %d x %d\", width, height, w, h)\n\t\t}\n\n\t\tgolden.RequireEqual(withSuffix(t, \"at-top\"), vt.View())\n\n\t\tvt.ScrollDown(1)\n\t\tgolden.RequireEqual(withSuffix(t, \"scrolled-plus-1\"), vt.View())\n\n\t\tvt.ScrollDown(1)\n\t\tgolden.RequireEqual(withSuffix(t, \"scrolled-plus-2\"), vt.View())\n\n\t\tvt.GotoBottom()\n\t\tgolden.RequireEqual(withSuffix(t, \"at-bottom\"), vt.View())\n\t})\n\n\tt.Run(\"view-50x15-softwrap-gutter\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\twidth := 50\n\t\theight := 15\n\n\t\tvt := New(WithWidth(width), WithHeight(height))\n\t\tvt.SoftWrap = true\n\t\tvt.Style = vt.Style.Border(lipgloss.RoundedBorder())\n\t\tvt.LeftGutterFunc = func(ctx GutterContext) string {\n\t\t\treturn \"  \"\n\t\t}\n\t\tvt.SetContent(textContentList)\n\n\t\tif w, h := lipgloss.Size(vt.View()); w != width || h != height {\n\t\t\tt.Errorf(\"view size should be %d x %d, got %d x %d\", width, height, w, h)\n\t\t}\n\n\t\tgolden.RequireEqual(withSuffix(t, \"at-top\"), vt.View())\n\n\t\tvt.ScrollDown(1)\n\t\tif w, h := lipgloss.Size(vt.View()); w != width || h != height {\n\t\t\tt.Errorf(\"view size should be %d x %d, got %d x %d\", width, height, w, h)\n\t\t}\n\t\tgolden.RequireEqual(withSuffix(t, \"scrolled-plus-1\"), vt.View())\n\n\t\tvt.ScrollDown(1)\n\t\tif w, h := lipgloss.Size(vt.View()); w != width || h != height {\n\t\t\tt.Errorf(\"view size should be %d x %d, got %d x %d\", width, height, w, h)\n\t\t}\n\t\tgolden.RequireEqual(withSuffix(t, \"scrolled-plus-2\"), vt.View())\n\n\t\tvt.GotoBottom()\n\t\tif w, h := lipgloss.Size(vt.View()); w != width || h != height {\n\t\t\tt.Errorf(\"view size should be %d x %d, got %d x %d\", width, height, w, h)\n\t\t}\n\t\tgolden.RequireEqual(withSuffix(t, \"at-bottom\"), vt.View())\n\t})\n\n\tt.Run(\"view-40x1-softwrap\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\twidth := 40 + 2 // +2 for border.\n\t\theight := 1 + 2 // +2 for border.\n\n\t\tvt := New(WithWidth(width), WithHeight(height))\n\t\tvt.SoftWrap = true\n\t\tvt.Style = vt.Style.Border(lipgloss.RoundedBorder())\n\t\tvt.SetContent(textContentList)\n\n\t\tview := vt.View()\n\t\tif w, h := lipgloss.Size(view); w != width || h != height {\n\t\t\tt.Errorf(\"view size should be %d x %d, got %d x %d\", width, height, w, h)\n\t\t}\n\n\t\tgolden.RequireEqual(t, view)\n\n\t\tvt.ScrollDown(1)\n\t\tgolden.RequireEqual(withSuffix(t, \"scrolled-plus-1\"), vt.View())\n\n\t\tvt.ScrollDown(1)\n\t\tgolden.RequireEqual(withSuffix(t, \"scrolled-plus-2\"), vt.View())\n\n\t\tvt.GotoBottom()\n\t\tgolden.RequireEqual(withSuffix(t, \"at-bottom\"), vt.View())\n\t})\n\n\tt.Run(\"view-50x15-content-lines\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tcontent := []string{\n\t\t\t\"57 Precepts of narcissistic comedy character Zote from an\\nawesome \\\"Hollow knight\\\" game\",\n\t\t}\n\t\tvt := New(WithWidth(50), WithHeight(15))\n\t\tvt.SetContentLines(content)\n\t\tgolden.RequireEqual(t, vt.View())\n\t})\n\n\tt.Run(\"view-0x0\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tvt := New(WithWidth(0), WithHeight(0))\n\t\tvt.SetContent(textContentList)\n\t\t_ = vt.View() // ensure no panic.\n\t})\n\tt.Run(\"view-1x0\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tvt := New(WithWidth(1), WithHeight(0))\n\t\tvt.SetContent(textContentList)\n\t\t_ = vt.View() // ensure no panic.\n\t})\n\tt.Run(\"view-0x1\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tvt := New(WithWidth(0), WithHeight(1))\n\t\tvt.SetContent(textContentList)\n\t\t_ = vt.View() // ensure no panic.\n\t})\n}\n\nfunc BenchmarkView(b *testing.B) {\n\tb.Run(\"view-30x15\", func(b *testing.B) {\n\t\tvt := New(WithWidth(30), WithHeight(15))\n\t\tvt.SetContent(textContentList)\n\n\t\tfor i := 0; i < b.N; i++ {\n\t\t\tvt.View()\n\t\t}\n\t})\n\n\tb.Run(\"view-100x100\", func(b *testing.B) {\n\t\tvt := New(WithWidth(100), WithHeight(100))\n\t\tvt.SetContent(textContentList)\n\n\t\tfor i := 0; i < b.N; i++ {\n\t\t\tvt.View()\n\t\t}\n\t})\n\n\tb.Run(\"view-30x15-softwrap\", func(b *testing.B) {\n\t\tvt := New(WithWidth(30), WithHeight(15))\n\t\tvt.SoftWrap = true\n\t\tvt.SetContent(textContentList)\n\n\t\tfor i := 0; i < b.N; i++ {\n\t\t\tvt.View()\n\t\t}\n\t})\n\n\tb.Run(\"view-100x100-softwrap\", func(b *testing.B) {\n\t\tvt := New(WithWidth(100), WithHeight(100))\n\t\tvt.SoftWrap = true\n\t\tvt.SetContent(textContentList)\n\n\t\tfor i := 0; i < b.N; i++ {\n\t\t\tvt.View()\n\t\t}\n\t})\n}\n"
  }
]