Full Code of charmbracelet/huh for AI

main 3b90d9d74396 cached
108 files
331.5 KB
101.3k tokens
687 symbols
1 requests
Download .txt
Showing preview only (357K chars total). Download the full file or copy to clipboard to get everything.
Repository: charmbracelet/huh
Branch: main
Commit: 3b90d9d74396
Files: 108
Total size: 331.5 KB

Directory structure:
gitextract_o580ujl4/

├── .gitattributes
├── .github/
│   ├── CODEOWNERS
│   ├── dependabot.yml
│   └── workflows/
│       ├── build.yml
│       ├── dependabot-sync.yml
│       ├── lint-sync.yml
│       ├── lint.yml
│       └── release.yml
├── .gitignore
├── .golangci.yml
├── .goreleaser.yml
├── LICENSE
├── Makefile
├── README.md
├── UPGRADE_GUIDE_V2.md
├── accessor.go
├── eval.go
├── examples/
│   ├── .gitignore
│   ├── accessibility/
│   │   ├── accessible.tape
│   │   └── main.go
│   ├── accessibility-secure-input/
│   │   └── main.go
│   ├── bubbletea/
│   │   ├── demo.tape
│   │   └── main.go
│   ├── bubbletea-options/
│   │   └── main.go
│   ├── burger/
│   │   ├── demo.tape
│   │   └── main.go
│   ├── conditional/
│   │   └── main.go
│   ├── dynamic/
│   │   ├── demo.tape
│   │   ├── dynamic-all/
│   │   │   └── main.go
│   │   ├── dynamic-bubbletea/
│   │   │   └── main.go
│   │   ├── dynamic-count/
│   │   │   └── main.go
│   │   ├── dynamic-country/
│   │   │   └── main.go
│   │   ├── dynamic-increment/
│   │   │   └── main.go
│   │   ├── dynamic-markdown/
│   │   │   └── main.go
│   │   ├── dynamic-name/
│   │   │   └── main.go
│   │   └── dynamic-suggestions/
│   │       └── main.go
│   ├── filepicker/
│   │   ├── artichoke.hs
│   │   ├── demo.tape
│   │   └── main.go
│   ├── filepicker-picking/
│   │   └── main.go
│   ├── gh/
│   │   └── create.go
│   ├── git/
│   │   └── main.go
│   ├── go.mod
│   ├── go.sum
│   ├── gum/
│   │   └── main.go
│   ├── help/
│   │   └── main.go
│   ├── hide/
│   │   ├── hide.tape
│   │   └── main.go
│   ├── layout/
│   │   ├── columns/
│   │   │   └── main.go
│   │   ├── default/
│   │   │   └── main.go
│   │   ├── grid/
│   │   │   └── main.go
│   │   └── stack/
│   │       └── main.go
│   ├── multiple-groups/
│   │   └── main.go
│   ├── readme/
│   │   ├── confirm/
│   │   │   ├── confirm.tape
│   │   │   └── main.go
│   │   ├── input/
│   │   │   ├── input.tape
│   │   │   ├── main.go
│   │   │   └── suggestions.tape
│   │   ├── main/
│   │   │   └── main.go
│   │   ├── multiselect/
│   │   │   ├── main.go
│   │   │   └── multiselect.tape
│   │   ├── note/
│   │   │   └── main.go
│   │   ├── select/
│   │   │   ├── main.go
│   │   │   ├── scroll/
│   │   │   │   ├── scroll.go
│   │   │   │   └── scroll.tape
│   │   │   └── select.tape
│   │   └── text/
│   │       ├── main.go
│   │       └── text.tape
│   ├── scroll/
│   │   └── main.go
│   ├── skip/
│   │   └── main.go
│   ├── spinner/
│   │   ├── accessible/
│   │   │   └── main.go
│   │   ├── context/
│   │   │   └── main.go
│   │   ├── context-and-action/
│   │   │   └── main.go
│   │   ├── context-and-action-and-error/
│   │   │   └── main.go
│   │   ├── loading/
│   │   │   ├── demo.tape
│   │   │   └── main.go
│   │   └── static/
│   │       └── main.go
│   ├── ssh-form/
│   │   └── main.go
│   ├── stickers/
│   │   └── main.go
│   ├── theme/
│   │   ├── main.go
│   │   └── theme.tape
│   └── timer/
│       └── main.go
├── field_confirm.go
├── field_filepicker.go
├── field_input.go
├── field_multiselect.go
├── field_note.go
├── field_select.go
├── field_text.go
├── form.go
├── go.mod
├── go.sum
├── group.go
├── huh.go
├── huh_test.go
├── internal/
│   ├── accessibility/
│   │   └── accessibility.go
│   ├── compat/
│   │   └── model.go
│   └── selector/
│       └── selector.go
├── keymap.go
├── layout.go
├── option.go
├── run.go
├── spinner/
│   ├── spinner.go
│   └── spinner_test.go
├── theme.go
├── validate.go
├── wrap.go
└── zz_resize_width_test.go

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

================================================
FILE: .gitattributes
================================================
*.gif filter=lfs diff=lfs merge=lfs -text
*.png filter=lfs diff=lfs merge=lfs -text


================================================
FILE: .github/CODEOWNERS
================================================
* @charmbracelet/everyone


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

updates:
  - package-ecosystem: "gomod"
    directory: "/"
    schedule:
      interval: "weekly"
      day: "monday"
      time: "05:00"
      timezone: "America/New_York"
    labels:
      - "dependencies"
    commit-message:
      prefix: "chore"
      include: "scope"
    groups:
      all:
        patterns:
          - "*"
    ignore:
      - dependency-name: github.com/charmbracelet/bubbletea/v2
        versions:
          - v2.0.0-beta1

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

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

  - package-ecosystem: "gomod"
    directory: "/examples"
    schedule:
      interval: "weekly"
      day: "monday"
      time: "05:00"
      timezone: "America/New_York"
    labels:
      - "dependencies"
    commit-message:
      prefix: "chore"
      include: "scope"
    groups:
      all:
        patterns:
          - "*"
    ignore:
      - dependency-name: github.com/charmbracelet/bubbletea/v2
        versions:
          - v2.0.0-beta1

  - package-ecosystem: "gomod"
    directory: "/spinner"
    schedule:
      interval: "weekly"
      day: "monday"
      time: "05:00"
      timezone: "America/New_York"
    labels:
      - "dependencies"
    commit-message:
      prefix: "chore"
      include: "scope"
    groups:
      all:
        patterns:
          - "*"
    ignore:
      - dependency-name: github.com/charmbracelet/bubbletea/v2
        versions:
          - v2.0.0-beta1


================================================
FILE: .github/workflows/build.yml
================================================
name: build
on: [push, pull_request]
jobs:
  build:
    uses: charmbracelet/meta/.github/workflows/build.yml@main

  build-go-mod:
    uses: charmbracelet/meta/.github/workflows/build.yml@main
    with:
      go-version: ""
      go-version-file: ./go.mod

  build-examples:
    uses: charmbracelet/meta/.github/workflows/build.yml@main
    with:
      go-version: ""
      go-version-file: ./examples/go.mod
      working-directory: ./examples


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

permissions:
  contents: write
  pull-requests: write

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


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

permissions:
  contents: write
  pull-requests: write

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


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

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


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

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

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

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


================================================
FILE: .gitignore
================================================
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Dependency directories (remove the comment below to include it)
# vendor/

# Go workspace file
go.work

# Debugging
debug.log


================================================
FILE: .golangci.yml
================================================
version: "2"
run:
  tests: false
linters:
  enable:
    - bodyclose
    - exhaustive
    - goconst
    - godot
    - gomoddirectives
    - goprintffuncname
    - gosec
    - misspell
    - nakedret
    - nestif
    - nilerr
    - noctx
    - nolintlint
    - prealloc
    - revive
    - rowserrcheck
    - sqlclosecheck
    - tparallel
    - unconvert
    - unparam
    - whitespace
    - wrapcheck
  exclusions:
    rules:
      - text: '(slog|log)\.\w+'
        linters:
          - noctx
    generated: lax
    presets:
      - common-false-positives
  settings:
    exhaustive:
      default-signifies-exhaustive: true
issues:
  max-issues-per-linter: 0
  max-same-issues: 0
formatters:
  enable:
    - gofumpt
    - goimports
  exclusions:
    generated: lax


================================================
FILE: .goreleaser.yml
================================================
includes:
  - from_url:
      url: charmbracelet/meta/main/goreleaser-lib.yaml

# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json



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

Copyright (c) 2023–2026 Charm

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

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

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


================================================
FILE: Makefile
================================================
.PHONY: spinner

$(V).SILENT:
test:
	go test ./...

spinner:
	cd spinner/examples/loading && go run .

burger:
	cd examples/burger && go run .

theme:
	cd examples/theme && go run .

gh:
	cd examples/gh && go run .


================================================
FILE: README.md
================================================
# Huh?

<p>
  <img src="https://stuff.charm.sh/huh/glenn.png" width="400" />
  <br><br>
  <a href="https://github.com/charmbracelet/huh/releases"><img src="https://img.shields.io/github/release/charmbracelet/huh.svg" alt="Latest Release"></a>
  <a href="https://pkg.go.dev/charm.land/huh/v2#section-documentation"><img src="https://godoc.org/github.com/golang/gddo?status.svg" alt="Go Docs"></a>
  <a href="https://github.com/charmbracelet/huh/actions"><img src="https://github.com/charmbracelet/huh/actions/workflows/build.yml/badge.svg?branch=main" alt="Build Status"></a>
</p>

A simple, powerful library for building interactive forms and prompts in the terminal.

<img alt="Running a burger form" width="600" src="https://vhs.charm.sh/vhs-3J4i6HE3yBmz6SUO3HqILr.gif">

`huh?` is easy to use in a standalone fashion, can be
[integrated into a Bubble Tea application](#what-about-bubble-tea), and contains
a first-class [accessible mode](#accessibility) for screen readers.

The above example is running from a single Go program ([source](./examples/burger/main.go)).

## Tutorial

Let’s build a form for ordering burgers. To start, we’ll import the library and
define a few variables where we'll store answers.

```go
package main

import "charm.land/huh/v2"

var (
    burger       string
    toppings     []string
    sauceLevel   int
    name         string
    instructions string
    discount     bool
)
```

`huh?` separates forms into groups (you can think of groups as pages). Groups
are made of fields (e.g. `Select`, `Input`, `Text`). We will set up three
groups for the customer to fill out.

```go
form := huh.NewForm(
    huh.NewGroup(
        // Ask the user for a base burger and toppings.
        huh.NewSelect[string]().
            Title("Choose your burger").
            Options(
                huh.NewOption("Charmburger Classic", "classic"),
                huh.NewOption("Chickwich", "chickwich"),
                huh.NewOption("Fishburger", "fishburger"),
                huh.NewOption("Charmpossible™ Burger", "charmpossible"),
            ).
            Value(&burger), // store the chosen option in the "burger" variable

        // Let the user select multiple toppings.
        huh.NewMultiSelect[string]().
            Title("Toppings").
            Options(
                huh.NewOption("Lettuce", "lettuce").Selected(true),
                huh.NewOption("Tomatoes", "tomatoes").Selected(true),
                huh.NewOption("Jalapeños", "jalapeños"),
                huh.NewOption("Cheese", "cheese"),
                huh.NewOption("Vegan Cheese", "vegan cheese"),
                huh.NewOption("Nutella", "nutella"),
            ).
            Limit(4). // there’s a 4 topping limit!
            Value(&toppings),

        // Option values in selects and multi selects can be any type you
        // want. We’ve been recording strings above, but here we’ll store
        // answers as integers. Note the generic "[int]" directive below.
        huh.NewSelect[int]().
            Title("How much Charm Sauce do you want?").
            Options(
                huh.NewOption("None", 0),
                huh.NewOption("A little", 1),
                huh.NewOption("A lot", 2),
            ).
            Value(&sauceLevel),
    ),

    // Gather some final details about the order.
    huh.NewGroup(
        huh.NewInput().
            Title("What’s your name?").
            Value(&name).
            // Validating fields is easy. The form will mark erroneous fields
            // and display error messages accordingly.
            Validate(func(str string) error {
                if str == "Frank" {
                    return errors.New("Sorry, we don’t serve customers named Frank.")
                }
                return nil
            }),

        huh.NewText().
            Title("Special Instructions").
            CharLimit(400).
            Value(&instructions),

        huh.NewConfirm().
            Title("Would you like 15% off?").
            Value(&discount),
    ),
)
```

Finally, run the form:

```go
err := form.Run()
if err != nil {
    log.Fatal(err)
}

if !discount {
    fmt.Println("What? You didn’t take the discount?!")
}
```

And that’s it! For more info see [the full source][burgersource] for this
example as well as [the docs][docs].

If you need more dynamic forms that change based on input from previous fields,
check out the [dynamic forms](#dynamic-forms) example.

[burgersource]: ./examples/burger/main.go
[docs]: https://pkg.go.dev/charm.land/huh/v2?tab=doc

## Field Reference

- [`Input`](#input): single line text input
- [`Text`](#text): multi-line text input
- [`Select`](#select): select an option from a list
- [`MultiSelect`](#multiple-select): select multiple options from a list
- [`Confirm`](#confirm): confirm an action (yes or no)

> [!TIP]
> Just want to prompt the user with a single field? Each field has a `Run`
> method that can be used as a shorthand for gathering quick and easy input.

```go
var name string

huh.NewInput().
    Title("What’s your name?").
    Value(&name).
    Run() // this is blocking...

fmt.Printf("Hey, %s!\n", name)
```

### Input

Prompt the user for a single line of text.

<img alt="Input field" width="600" src="https://vhs.charm.sh/vhs-1ULe9JbTHfwFmm3hweRVtD.gif">

```go
huh.NewInput().
    Title("What’s for lunch?").
    Prompt("?").
    Validate(isFood).
    Value(&lunch)
```

### Text

Prompt the user for multiple lines of text.

<img alt="Text field" width="600" src="https://vhs.charm.sh/vhs-2rrIuVSEf38bT0cwc8hfEG.gif">

```go
huh.NewText().
    Title("Tell me a story.").
    Validate(checkForPlagiarism).
    Value(&story)
```

### Select

Prompt the user to select a single option from a list.

<img alt="Select field" width="600" src="https://vhs.charm.sh/vhs-7wFqZlxMWgbWmOIpBqXJTi.gif">

```go
huh.NewSelect[string]().
    Title("Pick a country.").
    Options(
        huh.NewOption("United States", "US"),
        huh.NewOption("Germany", "DE"),
        huh.NewOption("Brazil", "BR"),
        huh.NewOption("Canada", "CA"),
    ).
    Value(&country)
```

### Multiple Select

Prompt the user to select multiple (zero or more) options from a list.

<img alt="Multiselect field" width="600" src="https://vhs.charm.sh/vhs-3TLImcoexOehRNLELysMpK.gif">

```go
huh.NewMultiSelect[string]().
    Options(
        huh.NewOption("Lettuce", "Lettuce").Selected(true),
        huh.NewOption("Tomatoes", "Tomatoes").Selected(true),
        huh.NewOption("Charm Sauce", "Charm Sauce"),
        huh.NewOption("Jalapeños", "Jalapeños"),
        huh.NewOption("Cheese", "Cheese"),
        huh.NewOption("Vegan Cheese", "Vegan Cheese"),
        huh.NewOption("Nutella", "Nutella"),
    ).
    Title("Toppings").
    Limit(4).
    Value(&toppings)
```

### Confirm

Prompt the user to confirm (Yes or No).

<img alt="Confirm field" width="600" src="https://vhs.charm.sh/vhs-2HeX5MdOxLsrWwsa0TNMIL.gif">

```go
huh.NewConfirm().
    Title("Are you sure?").
    Affirmative("Yes!").
    Negative("No.").
    Value(&confirm)
```

## Accessibility

`huh?` has a special rendering option designed specifically for screen readers.
You can enable it with `form.WithAccessible(true)`.

> [!TIP]
> We recommend setting this through an environment variable or configuration
> option to allow the user to control accessibility.

```go
accessibleMode := os.Getenv("ACCESSIBLE") != ""
form.WithAccessible(accessibleMode)
```

Accessible forms will drop TUIs in favor of standard prompts, providing better
dictation and feedback of the information on screen for the visually impaired.

<img alt="Accessible cuisine form" width="600" src="https://vhs.charm.sh/vhs-19xEBn4LgzPZDtgzXRRJYS.gif">

## Themes

`huh?` contains a powerful theme abstraction. Supply your own custom theme or
choose from one of the five predefined themes:

- `Charm`
- `Dracula`
- `Catppuccin`
- `Base 16`
- `Default`

<br />
<p>
    <img alt="Charm-themed form" width="400" src="https://stuff.charm.sh/huh/themes/charm-theme.png">
    <img alt="Dracula-themed form" width="400" src="https://stuff.charm.sh/huh/themes/dracula-theme.png">
    <img alt="Catppuccin-themed form" width="400" src="https://stuff.charm.sh/huh/themes/catppuccin-theme.png">
    <img alt="Base 16-themed form" width="400" src="https://stuff.charm.sh/huh/themes/basesixteen-theme.png">
    <img alt="Default-themed form" width="400" src="https://stuff.charm.sh/huh/themes/default-theme.png">
</p>

Themes can take advantage of the full range of
[Lip Gloss][lipgloss] style options. For a high level theme reference see
[the docs](https://pkg.go.dev/charm.land/huh/v2#Theme).

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

## Dynamic Forms

`huh?` forms can be as dynamic as your heart desires. Simply replace properties
with their equivalent `Func` to recompute the properties value every time a
different part of your form changes.

Here’s how you would build a simple country + state / province picker.

First, define some variables that we’ll use to store the user selection.

```go
var country string
var state string
```

Define your country select as you normally would:

```go
huh.NewSelect[string]().
    Options(huh.NewOptions("United States", "Canada", "Mexico")...).
    Value(&country).
    Title("Country").
```

Define your state select with `TitleFunc` and `OptionsFunc` instead of `Title`
and `Options`. This will allow you to change the title and options based on the
selection of the previous field, i.e. `country`.

To do this, we provide a `func() string` and a `binding any` to `TitleFunc`. The
function defines what to show for the title and the binding specifies what value
needs to change for the function to recompute. So if `country` changes (e.g. the
user changes the selection) we will recompute the function.

For `OptionsFunc`, we provide a `func() []Option[string]` and a `binding any`.
We’ll fetch the country’s states, provinces, or territories from an API. `huh`
will automatically handle caching for you.

> [!IMPORTANT]
> We have to pass `&country` as the binding to recompute the function only when
> `country` changes, otherwise we will hit the API too often.

```go
huh.NewSelect[string]().
    Value(&state).
    Height(8).
    TitleFunc(func() string {
        switch country {
        case "United States":
            return "State"
        case "Canada":
            return "Province"
        default:
            return "Territory"
        }
    }, &country).
    OptionsFunc(func() []huh.Option[string] {
        opts := fetchStatesForCountry(country)
        return huh.NewOptions(opts...)
    }, &country),
```

Lastly, run the `form` with these inputs.

```go
err := form.Run()
if err != nil {
    log.Fatal(err)
}
```

<img width="600" src="https://vhs.charm.sh/vhs-6FRmBjNi2aiRb4INPXwIjo.gif" alt="Country / State form with dynamic inputs running.">

## Bonus: Spinner

`huh?` ships with a standalone spinner package. It’s useful for indicating
background activity after a form is submitted.

<img alt="Spinner while making a burger" width="600" src="https://vhs.charm.sh/vhs-6HvYomAFP6H8mngOYWXvwJ.gif">

Create a new spinner, set a title, set the action (or provide a `Context`), and run the spinner:

<table>

<tr>
<td> <strong>Action Style</strong> </td><td> <strong>Context Style</strong> </td></tr>
<tr>
<td>

```go
err := spinner.New().
    Title("Making your burger...").
    Action(makeBurger).
    Run()

fmt.Println("Order up!")
```

</td>
<td>

```go
go makeBurger()

err := spinner.New().
    Type(spinner.Line).
    Title("Making your burger...").
    Context(ctx).
    Run()

fmt.Println("Order up!")
```

</td>
</tr>
</table>

For more on Spinners see the [spinner examples](./spinner/examples) and
[the spinner docs](https://pkg.go.dev/charm.land/huh/v2/spinner).

## What about Bubble Tea?

<img alt="Bubbletea + Huh?" width="174" src="https://stuff.charm.sh/huh/bubbletea-huh.png">

Huh is built on [Bubble Tea][tea] and, in addition to its standalone mode,
`huh?` has first-class support and can be easily integrated into
Bubble Tea applications. It’s very useful in portions of your Bubble Tea
application that need form-like input, and for times when you need more
flexibility than `huh?` alone can offer.

<img alt="Bubble Tea embedded form example" width="800" src="https://vhs.charm.sh/vhs-3wGaB7EUKWmojeaHpARMUv.gif">

A `huh.Form` is just a `tea.Model`, so you can use it just as
you would any other [Bubble](https://github.com/charmbracelet/bubbles).

```go
type Model struct {
    form *huh.Form // huh.Form is just a tea.Model
}

func NewModel() Model {
    return Model{
        form: huh.NewForm(
            huh.NewGroup(
                huh.NewSelect[string]().
                    Key("class").
                    Options(huh.NewOptions("Warrior", "Mage", "Rogue")...).
                    Title("Choose your class"),

            huh.NewSelect[int]().
                Key("level").
                Options(huh.NewOptions(1, 20, 9999)...).
                Title("Choose your level"),
            ),
        )
    }
}

func (m Model) Init() tea.Cmd {
    return m.form.Init()
}

func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    // ...

    form, cmd := m.form.Update(msg)
    if f, ok := form.(*huh.Form); ok {
        m.form = f
    }

    return m, cmd
}

func (m Model) View() string {
    if m.form.State == huh.StateCompleted {
        class := m.form.GetString("class")
        level := m.form.GetInt("level")
        return fmt.Sprintf("You selected: %s, Lvl. %d", class, level)
    }
    return m.form.View()
}

```

For more info in using `huh?` in Bubble Tea applications see [the full Bubble
Tea example][example].

[tea]: https://github.com/charmbracelet/bubbletea
[bubbles]: https://github.com/charmbracelet/bubbles
[example]: https://github.com/charmbracelet/huh/blob/main/examples/bubbletea/main.go

## `Huh?` in the Wild

For some `Huh?` programs in production, see:

* [glyphs](https://github.com/maaslalani/glyphs): a unicode symbol picker
* [meteor](https://github.com/stefanlogue/meteor): a highly customisable conventional commit message tool
* [freeze](https://github.com/charmbracelet/freeze): a tool for generating images of code and terminal output
* [savvy](https://github.com/getsavvyinc/savvy-cli): the easiest way to create, share, and run runbooks in the terminal

## Contributing

See [contributing][contribute].

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

## Feedback

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

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

## Acknowledgments

`huh?` is inspired by the wonderful [Survey][survey] library by Alec Aivazis.

[survey]: https://github.com/AlecAivazis/survey

## License

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

---

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

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

Charm热爱开源 • Charm loves open source • نحنُ نحب المصادر المفتوحة


================================================
FILE: UPGRADE_GUIDE_V2.md
================================================
# Huh v2 Upgrade Guide

This guide will help you migrate from Huh v1 to v2. Most changes are straightforward, and many are handled automatically by your IDE or `gofmt`.

> [!TIP]
> For a high-level overview of what's new, check out [What's New in Huh v2](WHATS_NEW_V2.md).

## Quick Start

Update your imports and dependencies, and you're 90% done:

```bash
# Update your go.mod
go get charm.land/huh/v2@latest
go get charm.land/bubbletea/v2@latest
go get charm.land/lipgloss/v2@latest
go get charm.land/bubbles/v2@latest
```

Then update your import paths:

```go
// Before
import (
    "github.com/charmbracelet/huh"
    "github.com/charmbracelet/huh/spinner"
    tea "github.com/charmbracelet/bubbletea"
    "github.com/charmbracelet/lipgloss"
    "github.com/charmbracelet/bubbles/key"
)

// After
import (
    "charm.land/huh/v2"
    "charm.land/huh/v2/spinner"
    tea "charm.land/bubbletea/v2"
    "charm.land/lipgloss/v2"
    "charm.land/bubbles/v2/key"
)
```

## Breaking Changes

### Import Paths

All Charm imports now use the `charm.land` vanity domain with a `/v2` version suffix.

| v1 | v2 |
|----|-----|
| `github.com/charmbracelet/huh` | `charm.land/huh/v2` |
| `github.com/charmbracelet/huh/spinner` | `charm.land/huh/v2/spinner` |
| `github.com/charmbracelet/bubbletea` | `charm.land/bubbletea/v2` |
| `github.com/charmbracelet/lipgloss` | `charm.land/lipgloss/v2` |
| `github.com/charmbracelet/bubbles` | `charm.land/bubbles/v2` |

### Theme Changes

Themes are now passed by value and take a `bool` parameter for dark mode detection.

**Before:**
```go
form := huh.NewForm(
    // ...
).WithTheme(huh.ThemeCharm())
```

**After:**
```go
isDark := lipgloss.HasDarkBackground() // or detect however you prefer
form := huh.NewForm(
    // ...
).WithTheme(huh.ThemeCharm(isDark))
```

All built-in themes now follow this pattern:

```go
huh.ThemeCharm(isDark bool) *Styles
huh.ThemeDracula(isDark bool) *Styles
huh.ThemeCatppuccin(isDark bool) *Styles
huh.ThemeBase(isDark bool) *Styles
huh.ThemeBase16(isDark bool) *Styles
```

### Theme Type Changes

The `Theme` type has changed from a struct to an interface:

**Before:**
```go
type Theme struct {
    Form FormStyles
    Group GroupStyles
    FieldSeparator lipgloss.Style
    Blurred FieldStyles
    Focused FieldStyles
    Help help.Styles
}
```

**After:**
```go
type ThemeFunc func(isDark bool) *Styles
```

If you created custom themes, you'll need to update them to this new function signature:

```go
func MyCustomTheme(isDark bool) *Styles {
    styles := &huh.Styles{
        // Your custom styles...
    }
    return styles
}
```

### Field-Level WithAccessible Removed

Individual fields no longer have `WithAccessible()` methods. Accessible mode is now controlled exclusively at the form level, making it simpler and more consistent.

**Before:**
```go
// v1 - each field could have its own accessible setting
input := huh.NewInput().
    Title("Name").
    WithAccessible(true)  // ❌ No longer exists

select := huh.NewSelect[string]().
    Title("Country").
    Options(huh.NewOptions("US", "CA", "MX")...).
    WithAccessible(true)  // ❌ Removed

confirm := huh.NewConfirm().
    Title("Continue?").
    WithAccessible(true)  // ❌ Gone from all field types

form := huh.NewForm(
    huh.NewGroup(input, select, confirm),
).WithAccessible(true)
```

**After:**
```go
// v2 - only the form controls accessible mode
input := huh.NewInput().
    Title("Name")

select := huh.NewSelect[string]().
    Title("Country").
    Options(huh.NewOptions("US", "CA", "MX")...)

confirm := huh.NewConfirm().
    Title("Continue?")

form := huh.NewForm(
    huh.NewGroup(input, select, confirm),
).WithAccessible(true)  // ✅ One setting for all fields
```

**Fields affected:**
- `Input.WithAccessible()` - removed
- `Text.WithAccessible()` - removed
- `Select.WithAccessible()` - removed
- `MultiSelect.WithAccessible()` - removed
- `Confirm.WithAccessible()` - removed
- `Note.WithAccessible()` - removed
- `FilePicker.WithAccessible()` - removed

The separate `github.com/charmbracelet/huh/accessibility` package is also gone. Just use `Form.WithAccessible()` directly.

### Bubble Tea v2 Integration

All methods that returned or accepted Bubble Tea types have been updated to v2:

**Field Methods:**
- `Blur() tea.Cmd` (now returns `charm.land/bubbletea/v2.Cmd`)
- `Focus() tea.Cmd` (now returns `charm.land/bubbletea/v2.Cmd`)
- `Init() tea.Cmd` (now returns `charm.land/bubbletea/v2.Cmd`)
- `Update(tea.Msg) (tea.Model, tea.Cmd)` (now uses v2 types)

**Form Methods:**
- `Init() tea.Cmd` (now returns `charm.land/bubbletea/v2.Cmd`)
- `Update(tea.Msg) (tea.Model, tea.Cmd)` (now uses v2 types)
- `WithProgramOptions(...tea.ProgramOption)` (now uses v2 types)

**Key Bindings:**
- `KeyBinds() []key.Binding` (now returns `charm.land/bubbles/v2/key.Binding`)

These changes are mostly mechanical. Your IDE should help you update these automatically.

### Lip Gloss v2 Types

All Lip Gloss types have been updated to v2. This affects style definitions in custom themes:

**Before:**
```go
import "github.com/charmbracelet/lipgloss"

style := lipgloss.NewStyle().
    Foreground(lipgloss.Color("205"))
```

**After:**
```go
import "charm.land/lipgloss/v2"

style := lipgloss.NewStyle().
    Foreground(lipgloss.Color("205"))
```

The API is largely the same, but the import path and internal types have changed.

### Position Type

Button alignment now uses Lip Gloss v2's `Position` type:

**Before:**
```go
import "github.com/charmbracelet/lipgloss"

field.WithButtonAlignment(lipgloss.Left)
```

**After:**
```go
import "charm.land/lipgloss/v2"

field.WithButtonAlignment(lipgloss.Left)
```

## New Features

### View Hooks

You can now modify the view before it's rendered:

```go
form.WithViewHook(func(v tea.View) tea.View {
    // Modify view properties like alt screen, mouse mode, etc.
    v.AltScreen = true
    return v
})
```

### Width Method

Select and MultiSelect fields now expose a `Width()` method for getting the field's width:

```go
width := multiSelect.Width()
```

### Model Type

The `Model` type is now exported, improving type safety when working with forms in Bubble Tea applications:

```go
var _ tea.Model = (*huh.Model)(nil)
```

## Migration Checklist

- [ ] Update `go.mod` dependencies to v2
- [ ] Update all import paths from `github.com/charmbracelet/` to `charm.land/` with `/v2` suffix
- [ ] Update theme calls to pass `isDark bool` parameter
- [ ] Remove field-level `WithAccessible()` calls (e.g., from `Input`, `Select`, etc.)
- [ ] Keep form-level `WithAccessible()` calls (those still work)
- [ ] Remove imports from `github.com/charmbracelet/huh/accessibility` package
- [ ] Update custom themes to `ThemeFunc` signature if applicable
- [ ] Run `go mod tidy`
- [ ] Run tests
- [ ] Update any documentation or examples

## Common Issues

### Import Cycles

If you encounter import cycle issues, make sure all Charm dependencies are on v2:

```bash
go list -m all | grep charmbracelet
go list -m all | grep charm.land
```

Ensure nothing is still referencing v1 versions.

### Type Mismatches

If you see type errors with `tea.Model`, `tea.Msg`, or `tea.Cmd`, double-check your Bubble Tea import:

```go
import tea "charm.land/bubbletea/v2"  // Make sure it's v2!
```

### Theme Signature Errors

If you get errors about theme functions, remember all built-in themes now require a `bool` parameter:

```go
// ✅ Correct
form.WithTheme(huh.ThemeCharm(true))

// ❌ Wrong
form.WithTheme(huh.ThemeCharm())
```

## Getting Help

If you run into issues:

- Check the [examples](./examples) directory for reference implementations
- Read the [Bubble Tea v2 Upgrade Guide](https://github.com/charmbracelet/bubbletea/blob/main/UPGRADE_GUIDE_V2.md)
- Ask in [Discord](https://charm.land/chat) or [Matrix](https://charm.land/matrix)
- Open an issue on [GitHub](https://github.com/charmbracelet/huh/issues)

---

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

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

Charm热爱开源 • Charm loves open source • نحنُ نحب المصادر المفتوحة


================================================
FILE: accessor.go
================================================
package huh

// Accessor give read/write access to field values.
type Accessor[T any] interface {
	Get() T
	Set(value T)
}

// EmbeddedAccessor is a basic accessor, acting as the default one for fields.
type EmbeddedAccessor[T any] struct {
	value T
}

// Get gets the value.
func (a *EmbeddedAccessor[T]) Get() T {
	return a.value
}

// Set sets the value.
func (a *EmbeddedAccessor[T]) Set(value T) {
	a.value = value
}

// PointerAccessor allows field value to be exposed as a pointed variable.
type PointerAccessor[T any] struct {
	value *T
}

// NewPointerAccessor returns a new pointer accessor.
func NewPointerAccessor[T any](value *T) *PointerAccessor[T] {
	return &PointerAccessor[T]{
		value: value,
	}
}

// Get gets the value.
func (a *PointerAccessor[T]) Get() T {
	return *a.value
}

// Set sets the value.
func (a *PointerAccessor[T]) Set(value T) {
	*a.value = value
}


================================================
FILE: eval.go
================================================
package huh

import (
	"time"

	"github.com/mitchellh/hashstructure/v2"
)

// Eval is an evaluatable value, it stores a cached value and a function to
// recompute it. It's bindings are what we check to see if we need to recompute
// the value.
//
// By default it is also cached.
type Eval[T any] struct {
	val T
	fn  func() T

	bindings     any
	bindingsHash uint64
	cache        map[uint64]T

	loading      bool
	loadingStart time.Time
}

const spinnerShowThreshold = 25 * time.Millisecond

func hash(val any) uint64 {
	hash, _ := hashstructure.Hash(val, hashstructure.FormatV2, nil)
	return hash
}

func (e *Eval[T]) shouldUpdate() (bool, uint64) {
	if e.fn == nil {
		return false, 0
	}
	newHash := hash(e.bindings)
	return e.bindingsHash != newHash, newHash
}

func (e *Eval[T]) loadFromCache() bool {
	val, ok := e.cache[e.bindingsHash]
	if ok {
		e.loading = false
		e.val = val
	}
	return ok
}

func (e *Eval[T]) update(val T) {
	e.val = val
	e.cache[e.bindingsHash] = val
	e.loading = false
}

type updateTitleMsg struct {
	id    int
	hash  uint64
	title string
}

type updateDescriptionMsg struct {
	id          int
	hash        uint64
	description string
}

type updatePlaceholderMsg struct {
	id          int
	hash        uint64
	placeholder string
}

type updateSuggestionsMsg struct {
	id          int
	hash        uint64
	suggestions []string
}

type updateOptionsMsg[T comparable] struct {
	id      int
	hash    uint64
	options []Option[T]
}


================================================
FILE: examples/.gitignore
================================================
.ssh


================================================
FILE: examples/accessibility/accessible.tape
================================================
Output accessible.gif

Set Height 600
Set Width 1000

Hide
  Type "go build -o accessible ." Enter
  Type "export ACCESSIBLE=true" Enter
  Type "clear && ./accessible"
  Enter
  Sleep 1s
Show

Sleep 1s

Type "2"
Sleep 500ms
Enter
Sleep 1.5s

Type "Souvlaki"
Sleep 1.5s
Enter
Sleep 1.5s

Hide
Type "rm accessible" Enter
Sleep 1s


================================================
FILE: examples/accessibility/main.go
================================================
package main

import (
	"log"

	"charm.land/huh/v2"
)

func main() {
	form := huh.NewForm(
		huh.NewGroup(
			huh.NewSelect[string]().
				Options(huh.NewOptions("Italian", "Greek", "Indian", "Japanese", "American")...).
				Title("Favorite Cuisine?"),
		),

		huh.NewGroup(
			huh.NewInput().
				Title("Favorite Meal?").
				Placeholder("Breakfast"),
		),
	).WithAccessible(true)

	err := form.Run()
	if err != nil {
		log.Fatal(err)
	}
}


================================================
FILE: examples/accessibility-secure-input/main.go
================================================
package main

import (
	"errors"
	"log"

	"charm.land/huh/v2"
)

func validate(s string) error {
	if s == "" {
		return errors.New("input cannot be empty")
	}
	return nil
}

func main() {
	form := huh.NewForm(
		huh.NewGroup(
			huh.NewNote().
				Title("Welcome!").
				Description("This is an accessible form example!"),
			huh.NewInput().
				Validate(validate).
				Title("Name:"),
			huh.NewInput().
				EchoMode(huh.EchoModePassword).
				Validate(validate).
				Title("Password:"),
			huh.NewMultiSelect[string]().
				Options(huh.NewOptions(
					"Red",
					"Green",
					"Yellow",
				)...).
				Limit(2).
				Title("Choose some colors:"),
			huh.NewSelect[string]().
				Options(huh.NewOptions(
					"Red",
					"Green",
					"Yellow",
				)...).
				Title("Choose the best color:"),
			huh.NewFilePicker().
				Title("Which file?"),
			huh.NewConfirm().
				Title("Send something?"),
		),
	).WithAccessible(true)

	err := form.Run()
	if err != nil {
		log.Fatal(err)
	}
}


================================================
FILE: examples/bubbletea/demo.tape
================================================
Set Height 775
Set Padding 60
Set Width 1200
Set FontSize 20

Hide
  Type "clear && go run ."
  Enter
  Sleep 1s
Show
Sleep 2s

Down Sleep 1s
Enter Sleep 1s
Down Sleep 1s
Enter Sleep 1s
Enter Sleep 1.5s
Left Sleep 2s
Enter Sleep 3s


================================================
FILE: examples/bubbletea/main.go
================================================
package main

import (
	"fmt"
	"image/color"
	"os"
	"strings"

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

const maxWidth = 80

type Styles struct {
	Base,
	HeaderText,
	Status,
	StatusHeader,
	Highlight,
	ErrorHeaderText,
	Help lipgloss.Style

	Red, Indigo, Green color.Color
}

func NewStyles(hasDarkBg bool) *Styles {
	var (
		s         = Styles{}
		lightDark = lipgloss.LightDark(hasDarkBg)
	)

	s.Red = lightDark(lipgloss.Color("#FE5F86"), lipgloss.Color("#FE5F86"))
	s.Indigo = lightDark(lipgloss.Color("#5A56E0"), lipgloss.Color("#7571F9"))
	s.Green = lightDark(lipgloss.Color("#02BA84"), lipgloss.Color("#02BF87"))
	s.Base = lipgloss.NewStyle().
		Padding(1, 4, 0, 1)
	s.HeaderText = lipgloss.NewStyle().
		Foreground(s.Indigo).
		Bold(true).
		Padding(0, 1, 0, 2)
	s.Status = lipgloss.NewStyle().
		Border(lipgloss.RoundedBorder()).
		BorderForeground(s.Indigo).
		PaddingLeft(1).
		MarginTop(1)
	s.StatusHeader = lipgloss.NewStyle().
		Foreground(s.Green).
		Bold(true)
	s.Highlight = lipgloss.NewStyle().
		Foreground(lipgloss.Color("212"))
	s.ErrorHeaderText = s.HeaderText.
		Foreground(s.Red)
	s.Help = lipgloss.NewStyle().
		Foreground(lipgloss.Color("240"))
	return &s
}

type state int

const (
	statusNormal state = iota
	stateDone
)

type Model struct {
	state     state
	styles    func(bool) *Styles
	form      *huh.Form
	hasDarkBg bool
	width     int
}

func NewModel() Model {
	m := Model{
		width:  maxWidth,
		styles: NewStyles,
	}

	m.form = huh.NewForm(
		huh.NewGroup(
			huh.NewSelect[string]().
				Key("class").
				Options(huh.NewOptions("Warrior", "Mage", "Rogue")...).
				Title("Choose your class").
				Description("This will determine your department"),

			huh.NewSelect[string]().
				Key("level").
				Options(huh.NewOptions("1", "20", "9999")...).
				Title("Choose your level").
				Description("This will determine your benefits package"),

			huh.NewConfirm().
				Key("done").
				Title("All done?").
				Validate(func(v bool) error {
					if !v {
						return fmt.Errorf("Welp, finish up then")
					}
					return nil
				}).
				Affirmative("Yep").
				Negative("Wait, no"),
		),
	).
		WithWidth(45).
		WithShowHelp(false).
		WithShowErrors(false)
	return m
}

func (m Model) Init() tea.Cmd {
	return m.form.Init()
}

func min(x, y int) int {
	if x > y {
		return y
	}
	return x
}

func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	styles := m.styles(m.hasDarkBg)
	switch msg := msg.(type) {
	case tea.BackgroundColorMsg:
		m.hasDarkBg = msg.IsDark()
	case tea.WindowSizeMsg:
		m.width = min(msg.Width, maxWidth) - styles.Base.GetHorizontalFrameSize()
	case tea.KeyPressMsg:
		switch msg.String() {
		case "ctrl+c":
			return m, tea.Interrupt
		case "esc", "q":
			return m, tea.Quit
		}
	}

	var cmds []tea.Cmd

	// Process the form
	form, cmd := m.form.Update(msg)
	if f, ok := form.(*huh.Form); ok {
		m.form = f
		cmds = append(cmds, cmd)
	}

	if m.form.State == huh.StateCompleted {
		// Quit when the form is done.
		cmds = append(cmds, tea.Quit)
	}

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

func (m Model) View() tea.View {
	s := m.styles(m.hasDarkBg)

	switch m.form.State {
	case huh.StateCompleted:
		title, role := m.getRole()
		title = s.Highlight.Render(title)
		var b strings.Builder
		fmt.Fprintf(&b, "Congratulations, you’re Charm’s newest\n%s!\n\n", title)
		fmt.Fprintf(&b, "Your job description is as follows:\n\n%s\n\nPlease proceed to HR immediately.", role)
		return tea.NewView(s.Status.Margin(0, 1).Padding(1, 2).Width(48).Render(b.String()) + "\n\n")
	default:

		var class string
		if m.form.GetString("class") != "" {
			class = "Class: " + m.form.GetString("class")
		}

		// Form (left side)
		v := strings.TrimSuffix(m.form.View(), "\n\n")
		form := lipgloss.NewStyle().Margin(1, 0).Render(v)

		// Status (right side)
		var status string
		{
			var (
				buildInfo      = "(None)"
				role           string
				jobDescription string
				level          string
			)

			if m.form.GetString("level") != "" {
				level = "Level: " + m.form.GetString("level")
				role, jobDescription = m.getRole()
				role = "\n\n" + s.StatusHeader.Render("Projected Role") + "\n" + role
				jobDescription = "\n\n" + s.StatusHeader.Render("Duties") + "\n" + jobDescription
			}
			if m.form.GetString("class") != "" {
				buildInfo = fmt.Sprintf("%s\n%s", class, level)
			}

			const statusWidth = 28
			statusMarginLeft := m.width - statusWidth - lipgloss.Width(form) - s.Status.GetMarginRight()
			status = s.Status.
				Height(lipgloss.Height(form)).
				Width(statusWidth).
				MarginLeft(statusMarginLeft).
				Render(s.StatusHeader.Render("Current Build") + "\n" +
					buildInfo +
					role +
					jobDescription)
		}

		errors := m.form.Errors()
		header := m.appBoundaryView("Charm Employment Application")
		if len(errors) > 0 {
			header = m.appErrorBoundaryView(m.errorView())
		}
		body := lipgloss.JoinHorizontal(lipgloss.Left, form, status)

		footer := m.appBoundaryView(m.form.Help().ShortHelpView(m.form.KeyBinds()))
		if len(errors) > 0 {
			footer = m.appErrorBoundaryView("")
		}

		return tea.NewView(s.Base.Render(header + "\n" + body + "\n\n" + footer))
	}
}

func (m Model) errorView() string {
	var s string
	for _, err := range m.form.Errors() {
		s += err.Error()
	}
	return s
}

func (m Model) appBoundaryView(text string) string {
	s := m.styles(m.hasDarkBg)
	return lipgloss.PlaceHorizontal(
		m.width,
		lipgloss.Left,
		s.HeaderText.Render(text),
		lipgloss.WithWhitespaceChars("/"),
		lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Foreground(s.Indigo)),
	)
}

func (m Model) appErrorBoundaryView(text string) string {
	s := m.styles(m.hasDarkBg)
	return lipgloss.PlaceHorizontal(
		m.width,
		lipgloss.Left,
		s.ErrorHeaderText.Render(text),
		lipgloss.WithWhitespaceChars("/"),
		lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Foreground(s.Red)),
	)
}

func (m Model) getRole() (string, string) {
	level := m.form.GetString("level")
	switch m.form.GetString("class") {
	case "Warrior":
		switch level {
		case "1":
			return "Tank Intern", "Assists with tank-related activities. Paid position."
		case "9999":
			return "Tank Manager", "Manages tanks and tank-related activities."
		default:
			return "Tank", "General tank. Does damage, takes damage. Responsible for tanking."
		}
	case "Mage":
		switch level {
		case "1":
			return "DPS Associate", "Finds DPS deals and passes them on to DPS Manager."
		case "9999":
			return "DPS Operating Officer", "Oversees all DPS activities."
		default:
			return "DPS", "Does damage and ideally does not take damage. Logs hours in JIRA."
		}
	case "Rogue":
		switch level {
		case "1":
			return "Stealth Junior Designer", "Designs rogue-like activities. Reports to Stealth Lead."
		case "9999":
			return "Stealth Lead", "Lead designer for all things stealth. Some travel required."
		default:
			return "Sneaky Person", "Sneaks around and does sneaky things. Reports to Stealth Lead."
		}
	default:
		return "", ""
	}
}

func main() {
	_, err := tea.NewProgram(NewModel()).Run()
	if err != nil {
		fmt.Println("Oh no:", err)
		os.Exit(1)
	}
}


================================================
FILE: examples/bubbletea-options/main.go
================================================
package main

import (
	"fmt"

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

func main() {
	var name string
	form := huh.NewForm(
		huh.NewGroup(huh.NewInput().Description("What should we call you?").Value(&name)),
	).WithViewHook(func(v tea.View) tea.View {
		v.AltScreen = true
		return v
	})

	err := form.Run()
	if err != nil {
		fmt.Println("error:", err)
	}

	fmt.Println("Welcome, " + name + "!")
}


================================================
FILE: examples/burger/demo.tape
================================================
Output burger.gif

Set Height 700
Set Width 1000

Hide
Type "go build -o burger ." Enter
Ctrl+L
Sleep 1s

Type "clear && ./burger"
Sleep 500ms
Enter
Sleep 500ms

Show

Sleep 1s
Type "n"
Sleep 1s
Down 2
Sleep 500ms Enter
Sleep 1s
Up@500ms
Sleep 500ms Enter
Sleep 500ms
Down@300ms 3
Sleep 300ms
Space
Sleep 750ms Enter
Sleep 500ms
Down@300ms 2
Sleep 500ms Enter
Sleep 750ms
Down@300ms
Sleep 500ms Enter

Sleep 1s

Type "Hilda"
Sleep 500ms Enter

Sleep 1s
Type "Extra spicy please!"
Sleep 500ms Tab

Sleep 750ms
Left
Sleep 750ms Enter

Sleep 5s

Hide
Type "rm burger" Enter
Sleep 1s


================================================
FILE: examples/burger/main.go
================================================
package main

import (
	"errors"
	"fmt"
	"os"
	"strconv"
	"strings"
	"time"

	"charm.land/huh/v2"
	"charm.land/huh/v2/spinner"
	"charm.land/lipgloss/v2"
	xstrings "github.com/charmbracelet/x/exp/strings"
)

type Spice int

const (
	Mild Spice = iota + 1
	Medium
	Hot
)

func (s Spice) String() string {
	switch s {
	case Mild:
		return "Mild "
	case Medium:
		return "Medium-Spicy "
	case Hot:
		return "Spicy-Hot "
	default:
		return ""
	}
}

type Order struct {
	Burger       Burger
	Side         string
	Name         string
	Instructions string
	Discount     bool
}

type Burger struct {
	Type     string
	Toppings []string
	Spice    Spice
}

func main() {
	var burger Burger
	order := Order{Burger: burger}

	// Should we run in accessible mode?
	accessible, _ := strconv.ParseBool(os.Getenv("ACCESSIBLE"))

	form := huh.NewForm(
		huh.NewGroup(huh.NewNote().
			Title("Charmburger").
			Description("Welcome to _Charmburger™_.\n\nHow may we take your order?").
			Next(true).
			NextLabel("Next"),
		),

		// Choose a burger.
		// We'll need to know what topping to add too.
		huh.NewGroup(
			huh.NewSelect[string]().
				Options(huh.NewOptions("Charmburger Classic", "Chickwich", "Fishburger", "Charmpossible™ Burger")...).
				Title("Choose your burger").
				Description("At Charm we truly have a burger for everyone.").
				Validate(func(t string) error {
					if t == "Fishburger" {
						return fmt.Errorf("no fish today, sorry")
					}
					return nil
				}).
				Value(&order.Burger.Type),

			huh.NewMultiSelect[string]().
				Title("Toppings").
				Description("Choose up to 4.").
				Options(
					huh.NewOption("Lettuce", "Lettuce").Selected(true),
					huh.NewOption("Tomatoes", "Tomatoes").Selected(true),
					huh.NewOption("Charm Sauce", "Charm Sauce"),
					huh.NewOption("Jalapeños", "Jalapeños"),
					huh.NewOption("Cheese", "Cheese"),
					huh.NewOption("Vegan Cheese", "Vegan Cheese"),
					huh.NewOption("Nutella", "Nutella"),
				).
				Validate(func(t []string) error {
					if len(t) <= 0 {
						return fmt.Errorf("at least one topping is required")
					}
					return nil
				}).
				Value(&order.Burger.Toppings).
				Filterable(true).
				Limit(4),
		),

		// Prompt for toppings and special instructions.
		// The customer can ask for up to 4 toppings.
		huh.NewGroup(
			huh.NewSelect[Spice]().
				Title("Spice level").
				Options(
					huh.NewOption("Mild", Mild).Selected(true),
					huh.NewOption("Medium", Medium),
					huh.NewOption("Hot", Hot),
				).
				Value(&order.Burger.Spice),

			huh.NewSelect[string]().
				Options(huh.NewOptions("Fries", "Disco Fries", "R&B Fries", "Carrots")...).
				Value(&order.Side).
				Title("Sides").
				Description("You get one free side with this order."),
		),

		// Gather final details for the order.
		huh.NewGroup(
			huh.NewInput().
				Value(&order.Name).
				Title("What's your name?").
				Placeholder("Margaret Thatcher").
				Validate(func(s string) error {
					if s == "Frank" {
						return errors.New("no franks, sorry")
					}
					return nil
				}).
				Description("For when your order is ready."),

			huh.NewText().
				Value(&order.Instructions).
				Placeholder("Just put it in the mailbox please").
				Title("Special Instructions").
				Description("Anything we should know?").
				CharLimit(400).
				Lines(5),

			huh.NewConfirm().
				Title("Would you like 15% off?").
				Value(&order.Discount).
				Affirmative("Yes!").
				Negative("No."),
		),
	).WithAccessible(accessible)

	err := form.Run()
	if err != nil {
		fmt.Println("Uh oh:", err)
		os.Exit(1)
	}

	prepareBurger := func() {
		time.Sleep(2 * time.Second)
	}

	_ = spinner.New().Title("Preparing your burger...").WithAccessible(accessible).Action(prepareBurger).Run()

	// Print order summary.
	{
		var sb strings.Builder
		keyword := func(s string) string {
			return lipgloss.NewStyle().Foreground(lipgloss.Color("212")).Render(s)
		}
		fmt.Fprintf(&sb,
			"%s\n\nOne %s%s, topped with %s with %s on the side.",
			lipgloss.NewStyle().Bold(true).Render("BURGER RECEIPT"),
			keyword(order.Burger.Spice.String()),
			keyword(order.Burger.Type),
			keyword(xstrings.EnglishJoin(order.Burger.Toppings, true)),
			keyword(order.Side),
		)

		name := order.Name
		if name != "" {
			name = ", " + name
		}
		fmt.Fprintf(&sb, "\n\nThanks for your order%s!", name)

		if order.Discount {
			fmt.Fprint(&sb, "\n\nEnjoy 15% off.")
		}

		fmt.Println(
			lipgloss.NewStyle().
				Width(40).
				BorderStyle(lipgloss.RoundedBorder()).
				BorderForeground(lipgloss.Color("63")).
				Padding(1, 2).
				Render(sb.String()),
		)
	}
}


================================================
FILE: examples/conditional/main.go
================================================
package main

import (
	"fmt"
	"os"

	"charm.land/huh/v2"
)

type consumable int

const (
	fruits consumable = iota
	vegetables
	drinks
)

func (c consumable) String() string {
	return [...]string{"fruit", "vegetable", "drink"}[c]
}

func main() {
	var category consumable
	type opts []huh.Option[string]

	var choice string

	// Then ask for a specific food item based on the previous answer.
	err := huh.NewForm(
		huh.NewGroup(
			huh.NewSelect[consumable]().
				Title("What are you in the mood for?").
				Value(&category).
				Options(
					huh.NewOption("Some fruit", fruits),
					huh.NewOption("A vegetable", vegetables),
					huh.NewOption("A drink", drinks),
				),

			huh.NewSelect[string]().
				Value(&choice).
				Height(7).
				TitleFunc(func() string {
					return fmt.Sprintf("Okay, what kind of %s are you in the mood for?", category)
				}, &category).
				OptionsFunc(func() []huh.Option[string] {
					switch category {
					case fruits:
						return []huh.Option[string]{
							huh.NewOption("Tangerine", "tangerine"),
							huh.NewOption("Canteloupe", "canteloupe"),
							huh.NewOption("Pomelo", "pomelo"),
							huh.NewOption("Grapefruit", "grapefruit"),
						}
					case vegetables:
						return []huh.Option[string]{
							huh.NewOption("Carrot", "carrot"),
							huh.NewOption("Jicama", "jicama"),
							huh.NewOption("Kohlrabi", "kohlrabi"),
							huh.NewOption("Fennel", "fennel"),
							huh.NewOption("Ginger", "ginger"),
						}
					default:
						return []huh.Option[string]{
							huh.NewOption("Coffee", "coffee"),
							huh.NewOption("Tea", "tea"),
							huh.NewOption("Bubble Tea", "bubble tea"),
							huh.NewOption("Agua Fresca", "agua-fresca"),
						}
					}
				}, &category),
		),
	).Run()
	if err != nil {
		fmt.Println("Trouble in food paradise:", err)
		os.Exit(1)
	}

	fmt.Printf("One %s coming right up!\n", choice)
}


================================================
FILE: examples/dynamic/demo.tape
================================================
Output dynamic.gif

Set Shell "bash"
Set FontSize 28
Set Width 1000
Set Height 700

Hide
  Type "clear && go build -o dynamic ./dynamic-country"
  Enter
  Sleep 1s
Show

Sleep 1s
Type "./dynamic" Sleep 500ms  Enter

Sleep 3.5s
Down
Sleep 2.5s
Down
Sleep 2.5s
Enter
Sleep 1s
Down@150ms 12
Up@150ms 2

Sleep 1s

Enter

Sleep 3s

Hide
  Type "rm dynamic"
Show


================================================
FILE: examples/dynamic/dynamic-all/main.go
================================================
package main

import (
	"log"
	"strconv"

	"charm.land/huh/v2"
)

func main() {
	var value string = "Dynamic"

	f := huh.NewForm(
		huh.NewGroup(
			huh.NewInput().Value(&value).Title("Dynamic").Description("Dynamic"),
			huh.NewNote().
				TitleFunc(func() string { return value }, &value).
				DescriptionFunc(func() string { return value }, &value),
			huh.NewSelect[string]().
				Height(7).
				TitleFunc(func() string { return value }, &value).
				DescriptionFunc(func() string { return value }, &value).
				OptionsFunc(func() []huh.Option[string] {
					var options []huh.Option[string]
					for i := 1; i < 6; i++ {
						options = append(options, huh.NewOption(value+" "+strconv.Itoa(i), value+strconv.Itoa(i)))
					}
					return options
				}, &value),
			huh.NewMultiSelect[string]().
				Height(7).
				TitleFunc(func() string { return value }, &value).
				DescriptionFunc(func() string { return value }, &value).
				OptionsFunc(func() []huh.Option[string] {
					var options []huh.Option[string]
					for i := 1; i < 6; i++ {
						options = append(options, huh.NewOption(value+" "+strconv.Itoa(i), value+strconv.Itoa(i)))
					}
					return options
				}, &value),
			huh.NewConfirm().
				TitleFunc(func() string { return value }, &value).
				DescriptionFunc(func() string { return value }, &value),
			huh.NewText().
				TitleFunc(func() string { return value }, &value).
				DescriptionFunc(func() string { return value }, &value).
				PlaceholderFunc(func() string { return value }, &value),
		),
	)
	err := f.Run()
	if err != nil {
		log.Fatal(err)
	}
}


================================================
FILE: examples/dynamic/dynamic-bubbletea/main.go
================================================
package main

import (
	"fmt"
	"image/color"
	"os"
	"strings"

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

const maxWidth = 80

type Styles struct {
	Base,
	HeaderText,
	Status,
	StatusHeader,
	Highlight,
	ErrorHeaderText,
	Help lipgloss.Style

	Red, Indigo, Green color.Color
}

func NewStyles(hasDarkBg bool) *Styles {
	var (
		s         = Styles{}
		lightDark = lipgloss.LightDark(hasDarkBg)
	)

	s.Red = lightDark(lipgloss.Color("#FE5F86"), lipgloss.Color("#FE5F86"))
	s.Indigo = lightDark(lipgloss.Color("#5A56E0"), lipgloss.Color("#7571F9"))
	s.Green = lightDark(lipgloss.Color("#02BA84"), lipgloss.Color("#02BF87"))
	s.Base = lipgloss.NewStyle().
		Padding(1, 4, 0, 1)
	s.HeaderText = lipgloss.NewStyle().
		Foreground(s.Indigo).
		Bold(true).
		Padding(0, 1, 0, 2)
	s.Status = lipgloss.NewStyle().
		Border(lipgloss.RoundedBorder()).
		BorderForeground(s.Indigo).
		PaddingLeft(1).
		MarginTop(1)
	s.StatusHeader = lipgloss.NewStyle().
		Foreground(s.Green).
		Bold(true)
	s.Highlight = lipgloss.NewStyle().
		Foreground(lipgloss.Color("212"))
	s.ErrorHeaderText = s.HeaderText.
		Foreground(s.Red)
	s.Help = lipgloss.NewStyle().
		Foreground(lipgloss.Color("240"))
	return &s
}

type state int

const (
	statusNormal state = iota
	stateDone
)

type Model struct {
	state     state
	styles    func(bool) *Styles
	hasDarkBg bool
	form      *huh.Form
	width     int
}

func NewModel() Model {
	m := Model{width: maxWidth, styles: NewStyles}

	var class string

	m.form = huh.NewForm(
		huh.NewGroup(
			huh.NewSelect[string]().
				Key("class").
				Value(&class).
				Options(huh.NewOptions("Warrior", "Mage", "Rogue")...).
				Title("Choose your class").
				Description("This will determine your department"),

			huh.NewSelect[string]().
				Key("level").
				OptionsFunc(func() []huh.Option[string] {
					switch class {
					case "Warrior":
						return huh.NewOptions("1", "20", "9999")
					case "Mage":
						return huh.NewOptions("10", "100", "1000")
					}
					return huh.NewOptions("1", "20", "9999")
				}, &class).
				Title("Choose your level").
				Description("This will determine your benefits package"),

			huh.NewConfirm().
				Key("done").
				Title("All done?").
				Validate(func(v bool) error {
					if !v {
						return fmt.Errorf("Welp, finish up then")
					}
					return nil
				}).
				Affirmative("Yep").
				Negative("Wait, no"),
		),
	).
		WithWidth(45).
		WithShowHelp(false).
		WithShowErrors(false)
	return m
}

func (m Model) Init() tea.Cmd {
	return m.form.Init()
}

func min(x, y int) int {
	if x > y {
		return y
	}
	return x
}

func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.BackgroundColorMsg:
		m.hasDarkBg = msg.IsDark()
	case tea.WindowSizeMsg:
		s := m.styles(m.hasDarkBg)
		m.width = min(msg.Width, maxWidth) - s.Base.GetHorizontalFrameSize()
	case tea.KeyPressMsg:
		switch msg.String() {
		case "esc", "ctrl+c", "q":
			return m, tea.Quit
		}
	}

	var cmds []tea.Cmd

	// Process the form
	form, cmd := m.form.Update(msg)
	if f, ok := form.(*huh.Form); ok {
		m.form = f
		cmds = append(cmds, cmd)
	}

	if m.form.State == huh.StateCompleted {
		// Quit when the form is done.
		cmds = append(cmds, tea.Quit)
	}

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

func (m Model) View() tea.View {
	s := m.styles(m.hasDarkBg)

	switch m.form.State {
	case huh.StateCompleted:
		title, role := m.getRole()
		title = s.Highlight.Render(title)
		var b strings.Builder
		fmt.Fprintf(&b, "Congratulations, you’re Charm’s newest\n%s!\n\n", title)
		fmt.Fprintf(&b, "Your job description is as follows:\n\n%s\n\nPlease proceed to HR immediately.", role)
		return tea.NewView(s.Status.Margin(0, 1).Padding(1, 2).Width(48).Render(b.String()) + "\n\n")
	default:

		var class string
		if m.form.GetString("class") != "" {
			class = "Class: " + m.form.GetString("class")
		}

		// Form (left side)
		v := strings.TrimSuffix(m.form.View(), "\n\n")
		form := lipgloss.NewStyle().Margin(1, 0).Render(v)

		// Status (right side)
		var status string
		{
			var (
				buildInfo      = "(None)"
				role           string
				jobDescription string
				level          string
			)

			if m.form.GetString("level") != "" {
				level = "Level: " + m.form.GetString("level")
				role, jobDescription = m.getRole()
				role = "\n\n" + s.StatusHeader.Render("Projected Role") + "\n" + role
				jobDescription = "\n\n" + s.StatusHeader.Render("Duties") + "\n" + jobDescription
			}
			if m.form.GetString("class") != "" {
				buildInfo = fmt.Sprintf("%s\n%s", class, level)
			}

			const statusWidth = 28
			statusMarginLeft := m.width - statusWidth - lipgloss.Width(form) - s.Status.GetMarginRight()
			status = s.Status.
				Height(lipgloss.Height(form)).
				Width(statusWidth).
				MarginLeft(statusMarginLeft).
				Render(s.StatusHeader.Render("Current Build") + "\n" +
					buildInfo +
					role +
					jobDescription)
		}

		errors := m.form.Errors()
		header := m.appBoundaryView("Charm Employment Application")
		if len(errors) > 0 {
			header = m.appErrorBoundaryView(m.errorView())
		}
		body := lipgloss.JoinHorizontal(lipgloss.Left, form, status)

		footer := m.appBoundaryView(m.form.Help().ShortHelpView(m.form.KeyBinds()))
		if len(errors) > 0 {
			footer = m.appErrorBoundaryView("")
		}

		return tea.NewView(s.Base.Render(header + "\n" + body + "\n\n" + footer))
	}
}

func (m Model) errorView() string {
	var s string
	for _, err := range m.form.Errors() {
		s += err.Error()
	}
	return s
}

func (m Model) appBoundaryView(text string) string {
	s := m.styles(m.hasDarkBg)
	return lipgloss.PlaceHorizontal(
		m.width,
		lipgloss.Left,
		s.HeaderText.Render(text),
		lipgloss.WithWhitespaceChars("/"),
		lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Foreground(s.Indigo)),
	)
}

func (m Model) appErrorBoundaryView(text string) string {
	s := m.styles(m.hasDarkBg)
	return lipgloss.PlaceHorizontal(
		m.width,
		lipgloss.Left,
		s.ErrorHeaderText.Render(text),
		lipgloss.WithWhitespaceChars("/"),
		lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Foreground(s.Red)),
	)
}

func (m Model) getRole() (string, string) {
	level := m.form.GetString("level")
	switch m.form.GetString("class") {
	case "Warrior":
		switch level {
		case "1":
			return "Tank Intern", "Assists with tank-related activities. Paid position."
		case "9999":
			return "Tank Manager", "Manages tanks and tank-related activities."
		default:
			return "Tank", "General tank. Does damage, takes damage. Responsible for tanking."
		}
	case "Mage":
		switch level {
		case "1":
			return "DPS Associate", "Finds DPS deals and passes them on to DPS Manager."
		case "9999":
			return "DPS Operating Officer", "Oversees all DPS activities."
		default:
			return "DPS", "Does damage and ideally does not take damage. Logs hours in JIRA."
		}
	case "Rogue":
		switch level {
		case "1":
			return "Stealth Junior Designer", "Designs rougue-like activities. Reports to Stealth Lead."
		case "9999":
			return "Stealth Lead", "Lead designer for all things stealth. Some travel required."
		default:
			return "Sneaky Person", "Sneaks around and does sneaky things. Reports to Stealth Lead."
		}
	default:
		return "", ""
	}
}

func main() {
	_, err := tea.NewProgram(NewModel()).Run()
	if err != nil {
		fmt.Println("Oh no:", err)
		os.Exit(1)
	}
}


================================================
FILE: examples/dynamic/dynamic-count/main.go
================================================
package main

import (
	"errors"
	"fmt"
	"log"
	"strconv"

	"charm.land/huh/v2"
)

func main() {
	var value string
	defaultValue := 10
	var chosen int

	f := huh.NewForm(
		huh.NewGroup(
			huh.NewInput().
				Value(&value).
				Title("Max").
				Placeholder(strconv.Itoa(defaultValue)).
				Validate(func(s string) error {
					v, err := strconv.Atoi(value)
					if err != nil {
						return errors.New("max should be a number")
					}
					if v <= 0 {
						return errors.New("maximum must be positive")
					}
					return nil
				}).
				Description("Select a maximum"),

			huh.NewSelect[int]().
				Value(&chosen).
				Title("Pick a number").
				DescriptionFunc(func() string {
					v, err := strconv.Atoi(value)
					if err != nil || v <= 0 {
						v = defaultValue
					}
					return "Between 1 and " + strconv.Itoa(v)
				}, &value).
				OptionsFunc(func() []huh.Option[int] {
					var options []huh.Option[int]
					v, err := strconv.Atoi(value)
					if err != nil {
						v = defaultValue
					}
					for i := range v {
						options = append(options, huh.NewOption(strconv.Itoa(i+1), i+1))
					}
					return options
				}, &value),
		),
	)
	err := f.Run()
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(chosen)
}


================================================
FILE: examples/dynamic/dynamic-country/main.go
================================================
package main

import (
	"fmt"
	"time"

	"charm.land/huh/v2"
	"charm.land/log/v2"
)

func main() {
	log.SetReportTimestamp(false)

	var (
		country string
		state   string
	)

	form := huh.NewForm(
		huh.NewGroup(
			huh.NewSelect[string]().
				Options(huh.NewOptions("United States", "Canada", "Mexico")...).
				Value(&country).
				Title("Country").
				Height(5),
			huh.NewSelect[string]().
				Value(&state).
				Height(8).
				TitleFunc(func() string {
					switch country {
					case "United States":
						return "State"
					case "Canada":
						return "Province"
					default:
						return "Territory"
					}
				}, &country).
				OptionsFunc(func() []huh.Option[string] {
					s := states[country]
					// simulate API call
					time.Sleep(1000 * time.Millisecond)
					return huh.NewOptions(s...)
				}, &country /* only this function when `country` changes */),
		),
	)

	err := form.Run()
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("%s, %s\n", state, country)
}

var states = map[string][]string{
	"Canada": {
		"Alberta",
		"British Columbia",
		"Manitoba",
		"New Brunswick",
		"Newfoundland and Labrador",
		"North West Territories",
		"Nova Scotia",
		"Nunavut",
		"Ontario",
		"Prince Edward Island",
		"Quebec",
		"Saskatchewan",
		"Yukon",
	},
	"Mexico": {
		"Aguascalientes",
		"Baja California",
		"Baja California Sur",
		"Campeche",
		"Chiapas",
		"Chihuahua",
		"Coahuila",
		"Colima",
		"Durango",
		"Guanajuato",
		"Guerrero",
		"Hidalgo",
		"Jalisco",
		"México",
		"Mexico City",
		"Michoacán",
		"Morelos",
		"Nayarit",
		"Nuevo León",
		"Oaxaca",
		"Puebla",
		"Querétaro",
		"Quintana Roo",
		"San Luis Potosí",
		"Sinaloa",
		"Sonora",
		"Tabasco",
		"Tamaulipas",
		"Tlaxcala",
		"Veracruz",
		"Ignacio de la Llave",
		"Yucatán",
		"Zacatecas",
	},
	"United States": {
		"Alabama",
		"Alaska",
		"Arizona",
		"Arkansas",
		"California",
		"Colorado",
		"Connecticut",
		"Delaware",
		"Florida",
		"Georgia",
		"Hawaii",
		"Idaho",
		"Illinois",
		"Indiana",
		"Iowa",
		"Kansas",
		"Kentucky",
		"Louisiana",
		"Maine",
		"Maryland",
		"Massachusetts",
		"Michigan",
		"Minnesota",
		"Mississippi",
		"Missouri",
		"Montana",
		"Nebraska",
		"Nevada",
		"New Hampshire",
		"New Jersey",
		"New Mexico",
		"New York",
		"North Carolina",
		"North Dakota",
		"Ohio",
		"Oklahoma",
		"Oregon",
		"Pennsylvania",
		"Rhode Island",
		"South Carolina",
		"South Dakota",
		"Tennessee",
		"Texas",
		"Utah",
		"Vermont",
		"Virginia",
		"Washington",
		"West Virginia",
		"Wisconsin",
		"Wyoming",
	},
}


================================================
FILE: examples/dynamic/dynamic-increment/main.go
================================================
package main

import (
	"fmt"
	"time"

	"charm.land/huh/v2"
)

func main() {
	count := 0
	go func() {
		for {
			count++
			time.Sleep(1 * time.Second)
		}
	}()

	descriptionFunc := func() string {
		return fmt.Sprintf("The count is: %d", count)
	}

	huh.NewForm(huh.NewGroup(
		huh.NewInput().
			Title("Fill in the input").
			DescriptionFunc(descriptionFunc, &count),
		huh.NewInput().
			Title("Fill in the input").
			DescriptionFunc(descriptionFunc, &count),
	)).Run()
}


================================================
FILE: examples/dynamic/dynamic-markdown/main.go
================================================
package main

import (
	"log"

	"charm.land/glamour/v2"
	"charm.land/huh/v2"
)

func main() {
	var md string
	err := huh.NewForm(
		huh.NewGroup(
			huh.NewText().Title("Markdown").Value(&md),
			huh.NewNote().Height(20).Title("Preview").
				DescriptionFunc(func() string {
					fmd, err := glamour.Render(md, "dark")
					if err != nil {
						return md
					}
					return fmd
				}, &md),
		),
	).Run()
	if err != nil {
		log.Fatal(err)
	}
}


================================================
FILE: examples/dynamic/dynamic-name/main.go
================================================
package main

import (
	"fmt"
	"log"

	"charm.land/huh/v2"
)

func main() {
	var name string

	err := huh.NewForm(
		huh.NewGroup(
			huh.NewInput().
				Title("What's your name?").
				Placeholder("Frank").
				Value(&name),
			huh.NewNote().
				TitleFunc(func() string {
					if name == "" {
						return "Hello!"
					}
					return fmt.Sprintf("Hello, %s!", name)
				}, &name).
				DescriptionFunc(func() string {
					if name == "" {
						return "How are you?"
					}
					return fmt.Sprintf("Your name is %d characters long", len(name))
				}, &name),
			huh.NewText().
				Title("Biography.").
				PlaceholderFunc(func() string {
					placeholder := "Tell me about yourself"
					if name != "" {
						placeholder += ", " + name
					}
					placeholder += "."
					return placeholder
				}, &name),
			huh.NewConfirm().
				TitleFunc(func() string {
					if name == "" {
						return "Continue?"
					}
					return fmt.Sprintf("Continue, %s?", name)
				}, &name).
				DescriptionFunc(func() string {
					if name == "" {
						return "Are you sure?"
					}
					return fmt.Sprintf("Last chance, %s.", name)
				}, &name),
		),
	).Run()
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println("Until next time, " + name + "!")
}


================================================
FILE: examples/dynamic/dynamic-suggestions/main.go
================================================
package main

import (
	"fmt"
	"log"

	"charm.land/huh/v2"
	"charm.land/huh/v2/spinner"
)

func main() {
	var org string
	var repo string

	err := huh.NewForm(
		huh.NewGroup(
			huh.NewInput().
				Value(&org).
				Title("Organization").
				Placeholder("charmbracelet"),
			huh.NewInput().
				Value(&repo).
				Title("Repository").
				PlaceholderFunc(func() string {
					switch org {
					case "hashicorp":
						return "terraform"
					case "golang":
						return "go"
					default: // charmbracelet
						return "bubbletea"
					}
				}, &org).
				SuggestionsFunc(func() []string {
					switch org {
					case "charmbracelet":
						return []string{"bubbletea", "huh", "mods", "melt", "freeze", "gum", "vhs", "pop", "lipgloss", "harmonica"}
					case "hashicorp":
						return []string{"terraform", "vault", "waypoint"}
					case "golang":
						return []string{"go", "net", "sys", "text", "tools"}
					default:
						return nil
					}
				}, &org),
		),
	).Run()
	if err != nil {
		log.Fatal(err)
	}

	spinner.New().Title(fmt.Sprintf("Cloning %s/%s...", org, repo)).Run()
}


================================================
FILE: examples/filepicker/artichoke.hs
================================================


================================================
FILE: examples/filepicker/demo.tape
================================================
Set Shell bash

Set Width 800
Set Height 725

Hide
  Type "clear && go build -o file"
  Enter
Show

Sleep .5s
Type "./file" Sleep .5s
Enter

Sleep 1s
Type "Frank" Sleep 500ms
Enter

Sleep 1s
Type "_frank" Sleep 500ms
Enter

Sleep 1s
Enter
Sleep 1s
Type@200ms "jjjj"
Sleep 1s
Enter

Sleep 1.5s
Type "hunter2"
Sleep 4s


================================================
FILE: examples/filepicker/main.go
================================================
package main

import (
	"charm.land/huh/v2"
)

func main() {
	var file string

	huh.NewForm(
		huh.NewGroup(
			huh.NewInput().
				Title("Name").
				Description("What's your name?"),

			huh.NewInput().
				Title("Username").
				Description("Select your username."),

			huh.NewFilePicker().
				Title("Profile").
				Description("Select your profile picture.").
				AllowedTypes([]string{".png", ".jpeg", ".webp", ".gif"}).
				Value(&file),

			huh.NewInput().
				Title("Password").
				EchoMode(huh.EchoModePassword).
				Description("Set your Password."),
		),
	).WithShowHelp(true).Run()
}


================================================
FILE: examples/filepicker-picking/main.go
================================================
package main

import (
	"fmt"

	"charm.land/huh/v2"
)

func main() {
	var file string
	huh.NewForm(
		huh.NewGroup(
			huh.NewFilePicker().
				Picking(true).
				Title("Code").
				Description("Select a .go file").
				AllowedTypes([]string{".go"}).
				Value(&file),
		),
	).WithShowHelp(true).Run()
	fmt.Println(file)
}


================================================
FILE: examples/gh/create.go
================================================
package main

import (
	"fmt"
	"log"
	"os"

	"charm.land/huh/v2"
	"charm.land/huh/v2/spinner"
	"charm.land/lipgloss/v2"
)

type Action int

const (
	Cancel Action = iota
	Push
	Fork
	Skip
)

var highlight = lipgloss.NewStyle().Foreground(lipgloss.Color("#00D7D7"))

func customTheme(isDark bool) *huh.Styles {
	theme := huh.ThemeBase16(isDark)
	theme.FieldSeparator = lipgloss.NewStyle().SetString("\n")
	theme.Help.FullKey.MarginTop(1)
	return theme
}

func main() {
	var action Action

	repo := "charmbracelet/huh"

	theme := spinner.ThemeFunc(func(isDark bool) *spinner.Styles {
		d := spinner.ThemeDefault(isDark)
		d.Spinner = lipgloss.NewStyle().Foreground(lipgloss.Color("4"))
		return d
	})

	f := huh.NewForm(
		huh.NewGroup(
			huh.NewSelect[Action]().
				Value(&action).
				Options(
					huh.NewOption(repo, Push),
					huh.NewOption("Create a fork of "+repo, Fork),
					huh.NewOption("Skip pushing the branch", Skip),
					huh.NewOption("Cancel", Cancel),
				).
				Title("Where should we push the 'feature' branch?"),
		),
	).WithTheme(huh.ThemeFunc(customTheme))

	err := f.Run()
	if err != nil {
		log.Fatal(err)
	}

	switch action {
	case Push:
		_ = spinner.New().Title("Pushing to charmbracelet/huh").WithTheme(theme).Run()
		fmt.Println("Pushed to charmbracelet/huh")
	case Fork:
		fmt.Println("Creating a fork of charmbracelet/huh...")
	case Skip:
		fmt.Println("Skipping pushing the branch...")
	case Cancel:
		fmt.Println("Cancelling...")
		os.Exit(1)
	}

	fmt.Printf("Creating pull request for %s into %s in %s\n", highlight.Render("test"), highlight.Render("main"), repo)

	var nextAction string

	f = huh.NewForm(
		huh.NewGroup(
			huh.NewInput().
				Title("Title ").
				Prompt("").
				Inline(true),
			huh.NewText().
				Title("Body"),
		),
		huh.NewGroup(
			huh.NewSelect[string]().
				Options(huh.NewOptions("Submit", "Submit as draft", "Continue in browser", "Add metadata", "Cancel")...).
				Title("What's next?").Value(&nextAction),
		),
	).WithTheme(huh.ThemeFunc(customTheme))

	err = f.Run()
	if err != nil {
		log.Fatal(err)
	}

	if nextAction == "Submit" {
		_ = spinner.New().Title("Submitting...").WithTheme(theme).Run()
		fmt.Println("Pull request submitted!")
	}
}


================================================
FILE: examples/git/main.go
================================================
package main

import (
	"charm.land/huh/v2"
)

// types is the possible commit types specified by the conventional commit spec.
var types = []string{"fix", "feat", "docs", "style", "refactor", "test", "chore", "revert"}

// This form is used to write a conventional commit message. It prompts the user
// to choose the type of commit as specified in the conventional commit spec.
// And then prompts for the summary and detailed description of the message and
// uses the values provided as the summary and details of the message.
func main() {
	var commit, scope string
	var summary, description string
	var confirm bool

	huh.NewForm(
		huh.NewGroup(
			huh.NewInput().Title("Type").Value(&commit).Placeholder("feat").Suggestions(types),
			huh.NewInput().Title("Scope").Value(&scope).Placeholder("scope"),
		),
		huh.NewGroup(
			huh.NewInput().Title("Summary").Value(&summary).Placeholder("Summary of changes"),
			huh.NewText().Title("Description").Value(&description).Placeholder("Detailed description of changes"),
		),
		huh.NewGroup(huh.NewConfirm().Title("Commit changes?").Value(&confirm)),
	).Run()
}


================================================
FILE: examples/go.mod
================================================
module examples

go 1.25.8

replace charm.land/huh/v2 => ../

require (
	charm.land/bubbles/v2 v2.0.0
	charm.land/bubbletea/v2 v2.0.2
	charm.land/glamour/v2 v2.0.0
	charm.land/huh/v2 v2.0.0-00010101000000-000000000000
	charm.land/lipgloss/v2 v2.0.2
	charm.land/log/v2 v2.0.0
	charm.land/wish/v2 v2.0.0
	github.com/charmbracelet/ssh v0.0.0-20250826160808-ebfa259c7309
	github.com/charmbracelet/x/exp/strings v0.1.0
)

require (
	github.com/alecthomas/chroma/v2 v2.14.0 // indirect
	github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
	github.com/atotto/clipboard v0.1.4 // indirect
	github.com/aymerick/douceur v0.2.0 // indirect
	github.com/catppuccin/go v0.3.0 // indirect
	github.com/charmbracelet/colorprofile v0.4.2 // indirect
	github.com/charmbracelet/harmonica v0.2.0 // indirect
	github.com/charmbracelet/keygen v0.5.4 // indirect
	github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect
	github.com/charmbracelet/x/ansi v0.11.6 // indirect
	github.com/charmbracelet/x/conpty v0.2.0 // indirect
	github.com/charmbracelet/x/exp/ordered v0.1.0 // indirect
	github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect
	github.com/charmbracelet/x/term v0.2.2 // indirect
	github.com/charmbracelet/x/termios v0.1.1 // indirect
	github.com/charmbracelet/x/windows v0.2.2 // indirect
	github.com/clipperhouse/displaywidth v0.11.0 // indirect
	github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
	github.com/creack/pty v1.1.24 // indirect
	github.com/dlclark/regexp2 v1.11.0 // indirect
	github.com/dustin/go-humanize v1.0.1 // indirect
	github.com/go-logfmt/logfmt v0.6.1 // indirect
	github.com/gorilla/css v1.0.1 // indirect
	github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
	github.com/mattn/go-runewidth v0.0.20 // indirect
	github.com/microcosm-cc/bluemonday v1.0.27 // indirect
	github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
	github.com/muesli/cancelreader v0.2.2 // indirect
	github.com/rivo/uniseg v0.4.7 // indirect
	github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
	github.com/yuin/goldmark v1.7.8 // indirect
	github.com/yuin/goldmark-emoji v1.0.5 // indirect
	golang.org/x/crypto v0.48.0 // indirect
	golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 // indirect
	golang.org/x/net v0.49.0 // indirect
	golang.org/x/sync v0.20.0 // indirect
	golang.org/x/sys v0.42.0 // indirect
	golang.org/x/text v0.34.0 // indirect
)


================================================
FILE: examples/go.sum
================================================
charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s=
charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI=
charm.land/bubbletea/v2 v2.0.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0=
charm.land/bubbletea/v2 v2.0.2/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ=
charm.land/glamour/v2 v2.0.0 h1:IDBoqLEy7Hdpb9VOXN+khLP/XSxtJy1VsHuW/yF87+U=
charm.land/glamour/v2 v2.0.0/go.mod h1:kjq9WB0s8vuUYZNYey2jp4Lgd9f4cKdzAw88FZtpj/w=
charm.land/lipgloss/v2 v2.0.2 h1:xFolbF8JdpNkM2cEPTfXEcW1p6NRzOWTSamRfYEw8cs=
charm.land/lipgloss/v2 v2.0.2/go.mod h1:KjPle2Qd3YmvP1KL5OMHiHysGcNwq6u83MUjYkFvEkM=
charm.land/log/v2 v2.0.0 h1:SY3Cey7ipx86/MBXQHwsguOT6X1exT94mmJRdzTNs+s=
charm.land/log/v2 v2.0.0/go.mod h1:c3cZSRqm20qUVVAR1WmS/7ab8bgha3C6G7DjPcaVZz0=
charm.land/wish/v2 v2.0.0 h1:0vryoDz6G1SdJNIWSkExy88dLAs7H/w0x9y/cay1vno=
charm.land/wish/v2 v2.0.0/go.mod h1:B42DmuVdvQxz215H9aCsbrXVSuAInAqkHAnmwg0nKs8=
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=
github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY=
github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8=
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/charmbracelet/keygen v0.5.4 h1:XQYgf6UEaTGgQSSmiPpIQ78WfseNQp4Pz8N/c1OsrdA=
github.com/charmbracelet/keygen v0.5.4/go.mod h1:t4oBRr41bvK7FaJsAaAQhhkUuHslzFXVjOBwA55CZNM=
github.com/charmbracelet/ssh v0.0.0-20250826160808-ebfa259c7309 h1:dCVbCRRtg9+tsfiTXTp0WupDlHruAXyp+YoxGVofHHc=
github.com/charmbracelet/ssh v0.0.0-20250826160808-ebfa259c7309/go.mod h1:R9cISUs5kAH4Cq/rguNbSwcR+slE5Dfm8FEs//uoIGE=
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 h1:eyFRbAmexyt43hVfeyBofiGSEmJ7krjLOYt/9CF5NKA=
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8/go.mod h1:SQpCTRNBtzJkwku5ye4S3HEuthAlGy2n9VXZnWkEW98=
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
github.com/charmbracelet/x/conpty v0.2.0 h1:eKtA2hm34qNfgJCDp/M6Dc0gLy7e07YEK4qAdNGOvVY=
github.com/charmbracelet/x/conpty v0.2.0/go.mod h1:fexgUnVrZgw8scD49f6VSi0Ggj9GWYIrpedRthAwW/8=
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=
github.com/charmbracelet/x/exp/ordered v0.1.0 h1:55/qLwjIh0gL0Vni+QAWk7T/qRVP6sBf+2agPBgnOFE=
github.com/charmbracelet/x/exp/ordered v0.1.0/go.mod h1:5UHwmG+is5THxMyCJHNPCn2/ecI07aKNrW+LcResjJ8=
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI=
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU=
github.com/charmbracelet/x/exp/strings v0.1.0 h1:i69S2XI7uG1u4NLGeJPSYU++Nmjvpo9nwd6aoEm7gkA=
github.com/charmbracelet/x/exp/strings v0.1.0/go.mod h1:/ehtMPNh9K4odGFkqYJKpIYyePhdp1hLBRvyY4bWkH8=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=
github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
github.com/charmbracelet/x/xpty v0.1.3 h1:eGSitii4suhzrISYH50ZfufV3v085BXQwIytcOdFSsw=
github.com/charmbracelet/x/xpty v0.1.3/go.mod h1:poPYpWuLDBFCKmKLDnhBp51ATa0ooD8FhypRwEFtH3Y=
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE=
github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=
github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=
github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 h1:zfMcR1Cs4KNuomFFgGefv5N0czO2XZpUbxGUy8i8ug0=
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=


================================================
FILE: examples/gum/main.go
================================================
package main

import (
	"fmt"
	"os"

	"charm.land/huh/v2"
)

func main() {
	if len(os.Args) < 2 {
		fmt.Println("gum <input | text>")
		os.Exit(1)
	}
	switch os.Args[1] {
	case "input":
		huh.NewInput().Run()
	case "text":
		huh.NewText().Run()
	case "confirm":
		huh.NewConfirm().Run()
	case "select":
		huh.NewSelect[string]().Options(huh.NewOptions(os.Args[2:]...)...).Run()
	case "multiselect":
		huh.NewMultiSelect[string]().Options(huh.NewOptions(os.Args[2:]...)...).Run()
	}
}


================================================
FILE: examples/help/main.go
================================================
package main

import "charm.land/huh/v2"

func main() {
	f := huh.NewForm(
		huh.NewGroup(
			huh.NewInput().Title("Dynamic Help"),
			huh.NewInput().Title("Dynamic Help"),
			huh.NewInput().Title("Dynamic Help"),
		),
	)
	f.Run()
}


================================================
FILE: examples/hide/hide.tape
================================================
Output hide.gif

Set Width 700
Set Padding 40
Set Height 350
Set FontSize 28

Hide
  Type "go build ."
  Sleep 500ms
  Enter
  Ctrl+L
  Sleep 500ms
  Type "clear && ./hide"
  Sleep 500ms
  Enter
  Sleep 500ms
Show

Type@500ms "llllllll"

Hide
Type "rm hide" Enter



================================================
FILE: examples/hide/main.go
================================================
package main

import (
	"fmt"

	"charm.land/huh/v2"
)

func main() {
	var isAllergic bool
	var allergies string

	huh.NewForm(
		huh.NewGroup(huh.NewNote().Title("Just for fun!")).WithHideFunc(func() bool { return true }),
		huh.NewGroup(huh.NewNote().Title("Just for fun!")).WithHide(true),

		huh.NewGroup(huh.NewConfirm().
			Title("Do you have any allergies?").
			Description("If so, please list them.").
			Value(&isAllergic)),
		huh.NewGroup(
			huh.NewText().
				Title("Allergies").
				Description("Please list all your allergies...").
				Value(&allergies),
		).WithHideFunc(func() bool {
			return !isAllergic
		}),
		huh.NewGroup(huh.NewNote().Title("Invisible")).WithHide(true),
	).Run()

	if isAllergic {
		fmt.Println(allergies)
	}
}


================================================
FILE: examples/layout/columns/main.go
================================================
package main

import "charm.land/huh/v2"

func main() {
	form := huh.NewForm(
		huh.NewGroup(
			huh.NewInput().Title("First"),
			huh.NewInput().Title("Second"),
			huh.NewInput().Title("Third"),
		),
		huh.NewGroup(
			huh.NewInput().Title("Fourth"),
			huh.NewInput().Title("Fifth"),
			huh.NewInput().Title("Sixth"),
		),
		huh.NewGroup(
			huh.NewInput().Title("Seventh"),
			huh.NewInput().Title("Eighth"),
			huh.NewInput().Title("Nineth"),
			huh.NewInput().Title("Tenth"),
		),
	).WithLayout(huh.LayoutColumns(2))
	form.Run()
}


================================================
FILE: examples/layout/default/main.go
================================================
package main

import "charm.land/huh/v2"

func main() {
	form := huh.NewForm(
		huh.NewGroup(
			huh.NewInput().Title("First"),
			huh.NewInput().Title("Second"),
			huh.NewInput().Title("Third"),
		),
		huh.NewGroup(
			huh.NewInput().Title("Fourth"),
			huh.NewInput().Title("Fifth"),
			huh.NewInput().Title("Sixth"),
		),
		huh.NewGroup(
			huh.NewInput().Title("Seventh"),
			huh.NewInput().Title("Eighth"),
			huh.NewInput().Title("Nineth"),
			huh.NewInput().Title("Tenth"),
		),
	)
	form.Run()
}


================================================
FILE: examples/layout/grid/main.go
================================================
package main

import "charm.land/huh/v2"

func main() {
	form := huh.NewForm(
		huh.NewGroup(
			huh.NewInput().Title("First"),
			huh.NewInput().Title("Second"),
			huh.NewInput().Title("Third"),
		),
		huh.NewGroup(
			huh.NewInput().Title("Fourth"),
			huh.NewInput().Title("Fifth"),
			huh.NewInput().Title("Sixth"),
		),
		huh.NewGroup(
			huh.NewInput().Title("Seventh"),
			huh.NewInput().Title("Eighth"),
			huh.NewInput().Title("Nineth"),
			huh.NewInput().Title("Tenth"),
		),
		huh.NewGroup(
			huh.NewInput().Title("Eleventh"),
			huh.NewInput().Title("Twelveth"),
			huh.NewInput().Title("Thirteenth"),
		),
	).WithLayout(huh.LayoutGrid(2, 2))
	form.Run()
}


================================================
FILE: examples/layout/stack/main.go
================================================
package main

import "charm.land/huh/v2"

func main() {
	form := huh.NewForm(
		huh.NewGroup(
			huh.NewInput().Title("First"),
			huh.NewInput().Title("Second"),
			huh.NewInput().Title("Third"),
		),
		huh.NewGroup(
			huh.NewInput().Title("Fourth"),
			huh.NewInput().Title("Fifth"),
			huh.NewInput().Title("Sixth"),
		),
		huh.NewGroup(
			huh.NewInput().Title("Seventh"),
			huh.NewInput().Title("Eighth"),
			huh.NewInput().Title("Nineth"),
			huh.NewInput().Title("Tenth"),
		),
	).WithLayout(huh.LayoutStack)
	form.Run()
}


================================================
FILE: examples/multiple-groups/main.go
================================================
package main

import (
	"fmt"
	"os"

	"charm.land/huh/v2"
)

func main() {
	f := huh.NewForm(
		huh.NewGroup(
			huh.NewSelect[string]().
				Options(
					huh.NewOption("A", "a"),
					huh.NewOption("B", "b"),
					huh.NewOption("C", "c"),
					huh.NewOption("D", "d"),
					huh.NewOption("E", "e"),
					huh.NewOption("F", "f"),
					huh.NewOption("G", "g"),
					huh.NewOption("H", "h"),
					huh.NewOption("I", "i"),
					huh.NewOption("J", "j"),
					huh.NewOption("K", "k").Selected(true),
					huh.NewOption("L", "l"),
					huh.NewOption("M", "m"),
					huh.NewOption("N", "n"),
					huh.NewOption("O", "o"),
					huh.NewOption("P", "p"),
				),
		).WithHeight(8),
		huh.NewGroup(
			huh.NewMultiSelect[string]().
				Options(
					huh.NewOption("A", "a"),
					huh.NewOption("B", "b"),
					huh.NewOption("C", "c"),
					huh.NewOption("D", "d"),
					huh.NewOption("E", "e"),
					huh.NewOption("F", "f"),
					huh.NewOption("G", "g"),
					huh.NewOption("H", "h"),
					huh.NewOption("I", "i"),
					huh.NewOption("K", "k").Selected(true),
					huh.NewOption("L", "l"),
					huh.NewOption("M", "m"),
					huh.NewOption("N", "n"),
					huh.NewOption("O", "o").Selected(true),
					huh.NewOption("P", "p"),
				),
		).WithHeight(10),
		huh.NewGroup(
			huh.NewSelect[string]().
				Options(
					huh.NewOption("A", "a"),
					huh.NewOption("B", "b"),
					huh.NewOption("C", "c"),
					huh.NewOption("D", "d"),
					huh.NewOption("E", "e"),
					huh.NewOption("F", "f"),
					huh.NewOption("G", "g"),
					huh.NewOption("H", "h"),
					huh.NewOption("I", "i"),
					huh.NewOption("J", "j"),
					huh.NewOption("K", "k").Selected(true),
					huh.NewOption("L", "l"),
					huh.NewOption("M", "m"),
					huh.NewOption("N", "n"),
					huh.NewOption("O", "o"),
					huh.NewOption("P", "p"),
				),
		).WithHeight(5),
	)

	if err := f.Run(); err != nil {
		fmt.Fprintf(os.Stderr, "Oof: %v\n", err)
	}
}


================================================
FILE: examples/readme/confirm/confirm.tape
================================================
Output confirm.gif

Set Width 1100
Set Padding 40
Set Height 375
Set FontSize 36

Hide
  Type "go build ."
  Sleep 500ms
  Enter
  Ctrl+L
  Sleep 500ms
  Type "clear && ./confirm"
  Sleep 500ms
  Enter
  Sleep 500ms
Show

Sleep 1s
Left@500ms 6
Sleep 2s

Hide
Type "rm confirm"
Enter
Sleep 500ms


================================================
FILE: examples/readme/confirm/main.go
================================================
package main

import (
	"charm.land/huh/v2"
)

func main() {
	var happy bool

	confirm := huh.NewConfirm().
		Title("Are you sure? ").
		Description("Please confirm. ").
		Affirmative("Yes!").
		Negative("No.").
		Value(&happy)

	huh.NewForm(huh.NewGroup(confirm)).Run()
}


================================================
FILE: examples/readme/input/input.tape
================================================
Output input.gif

Set Width 1000
Set Padding 30
Set Height 275
Set FontSize 38

Hide
  Type "go build ."
  Sleep 500ms
  Enter
  Ctrl+L
  Sleep 500ms
  Type "./input"
  Sleep 500ms
  Enter
  Sleep 500ms
Show

Sleep 1s
Type "Spaghetti"
Sleep 2s

Hide
Type "rm input"
Enter


================================================
FILE: examples/readme/input/main.go
================================================
package main

import (
	"fmt"

	"charm.land/huh/v2"
)

func isFood(_ string) error {
	return nil
}

func main() {
	var lunch string

	input := huh.NewInput().
		Title("What's for lunch?").
		Prompt("? ").
		Suggestions([]string{
			"Artichoke",
			"Baking Flour",
			"Bananas",
			"Barley",
			"Bean Sprouts",
			"Bitter Melon",
			"Black Cod",
			"Blood Orange",
			"Brown Sugar",
			"Cashew Apple",
			"Cashews",
			"Cat Food",
			"Coconut Milk",
			"Cucumber",
			"Curry Paste",
			"Currywurst",
			"Dill",
			"Dragonfruit",
			"Dried Shrimp",
			"Eggs",
			"Fish Cake",
			"Furikake",
			"Garlic",
		}).
		Validate(isFood).
		Value(&lunch)

	huh.NewForm(huh.NewGroup(input)).Run()

	fmt.Printf("Yummy, %s!\n", lunch)
}


================================================
FILE: examples/readme/input/suggestions.tape
================================================
Output suggestions.gif

Set Width 1000
Set Padding 30
Set Height 275
Set FontSize 38

Hide
  Type "go build ."
  Sleep 500ms
  Enter
  Ctrl+L
  Sleep 500ms
  Type "./input"
  Sleep 500ms
  Enter
  Sleep 500ms
Show

Sleep 1s
Type@300ms "Curryw"
Sleep 1.5s
Ctrl+E
Sleep 3s

Hide
Type "rm input"
Enter
Sleep 0.5s



================================================
FILE: examples/readme/main/main.go
================================================
package main

import (
	"log"

	"charm.land/huh/v2"
)

// TODO: ensure input is not plagiarized.
func checkForPlagiarism(s string) error { return nil }

// TODO: ensure input is food.
func isFood(s string) error { return nil }

// TODO: ensure input is a valid name.
func validateName(s string) error { return nil }

func main() {
	var (
		lunch    string
		story    string
		country  string
		toppings []string
		discount bool
	)

	// `Input`s are single line text fields.
	huh.NewInput().
		Title("What's for lunch?").
		Prompt("?").
		Validate(isFood).
		Value(&lunch)

	// `Text`s are multi-line text fields.
	huh.NewText().
		Title("Tell me a story.").
		Validate(checkForPlagiarism).
		Value(&story)

	// `Select`s are multiple choice questions.
	huh.NewSelect[string]().
		Title("Pick a country.").
		Options(
			huh.NewOption("United States", "US"),
			huh.NewOption("Germany", "DE"),
			huh.NewOption("Brazil", "BR"),
			huh.NewOption("Canada", "CA"),
		).
		Value(&country)

	// `MultiSelect`s allow multiple selections from a list of options.
	huh.NewMultiSelect[string]().
		Options(
			huh.NewOption("Cheese", "cheese").Selected(true),
			huh.NewOption("Lettuce", "lettuce").Selected(true),
			huh.NewOption("Corn", "corn"),
			huh.NewOption("Salsa", "salsa"),
			huh.NewOption("Sour Cream", "sour cream"),
			huh.NewOption("Tomatoes", "tomatoes"),
		).
		Title("Toppings").
		Limit(4).
		Value(&toppings)

	// `Confirm`s are a confirmation prompt.
	huh.NewConfirm().
		Title("Want a discount?").
		Affirmative("Yes!").
		Negative("No.").
		Value(&discount)

	// Form
	var (
		burger       string
		name         string
		instructions string
	)

	form := huh.NewForm(
		// Prompt the user to choose a burger.
		huh.NewGroup(
			huh.NewSelect[string]().
				Options(
					huh.NewOption("Charmburger Classic", "classic"),
					huh.NewOption("Chickwich", "chickwich"),
					huh.NewOption("Fishburger", "Fishburger"),
					huh.NewOption("Charmpossible™ Burger", "charmpossible"),
				).
				Title("Choose your burger").
				Value(&burger),
		),

		// Prompt for toppings and special instructions.
		// The customer can ask for up to 4 toppings.
		huh.NewGroup(
			huh.NewMultiSelect[string]().
				Options(
					huh.NewOption("Lettuce", "Lettuce").Selected(true),
					huh.NewOption("Tomatoes", "Tomatoes").Selected(true),
					huh.NewOption("Charm Sauce", "Charm Sauce"),
					huh.NewOption("Jalapeños", "Jalapeños"),
					huh.NewOption("Cheese", "Cheese"),
					huh.NewOption("Vegan Cheese", "Vegan Cheese"),
					huh.NewOption("Nutella", "Nutella"),
				).
				Title("Toppings").
				Limit(4).
				Value(&toppings),
		),

		// Gather final details for the order.
		huh.NewGroup(
			huh.NewInput().
				Title("What's your name?").
				Value(&name).
				Validate(validateName),

			huh.NewText().
				Title("Special Instructions").
				Value(&instructions).
				CharLimit(400),

			huh.NewConfirm().
				Title("Would you like 15% off").
				Value(&discount),
		),
	)

	err := form.Run()
	if err != nil {
		log.Fatal(err)
	}
}


================================================
FILE: examples/readme/multiselect/main.go
================================================
package main

import "charm.land/huh/v2"

func main() {
	var toppings []string
	s := huh.NewMultiSelect[string]().
		Options(
			huh.NewOption("Lettuce", "Lettuce").Selected(true),
			huh.NewOption("Tomatoes", "Tomatoes").Selected(true),
			huh.NewOption("Charm Sauce", "Charm Sauce"),
			huh.NewOption("Jalapeños", "Jalapeños"),
			huh.NewOption("Cheese", "Cheese"),
			huh.NewOption("Vegan Cheese", "Vegan Cheese"),
			huh.NewOption("Nutella", "Nutella"),
		).
		Title("Toppings").
		Limit(4).
		Value(&toppings)

	huh.NewForm(huh.NewGroup(s)).Run()
}


================================================
FILE: examples/readme/multiselect/multiselect.tape
================================================
Output multiselect.gif

Set Width 1150
Set Padding 40
Set Height 480
Set FontSize 28

Hide
  Type "go build ."
  Sleep 500ms
  Enter
  Ctrl+L
  Sleep 500ms
  Type "clear && ./multiselect"
  Sleep 500ms
  Enter
  Sleep 500ms
Show

Sleep 1s
Down@500ms 3
Sleep 1s
Type "x"
Sleep 1s
Up@500ms 2
Sleep 1s

Type "x"
Sleep 2s

Hide
Type "rm multiselect" Enter
Sleep 1s


================================================
FILE: examples/readme/note/main.go
================================================
package main

import "charm.land/huh/v2"

func main() {
	note := huh.NewNote().Description(
		"# Heading\n" + "This is _italic_, *bold*" +
			"\n\n# Heading\n" + "`This is _italic_, *bold*`",
	)
	huh.NewForm(
		huh.NewGroup(note),
	).Run()
}


================================================
FILE: examples/readme/select/main.go
================================================
package main

import "charm.land/huh/v2"

func main() {
	var country string
	s := huh.NewSelect[string]().
		Title("Pick a country.").
		Options(
			huh.NewOption("United States", "US"),
			huh.NewOption("Germany", "DE"),
			huh.NewOption("Brazil", "BR"),
			huh.NewOption("Canada", "CA"),
		).
		Value(&country)

	huh.NewForm(huh.NewGroup(s)).Run()
}


================================================
FILE: examples/readme/select/scroll/scroll.go
================================================
package main

import "charm.land/huh/v2"

type Pokemon struct {
	id   int
	name string
}

var pokemons = []Pokemon{
	{1, "Bulbasaur"},
	{2, "Ivysaur"},
	{3, "Venusaur"},
	{4, "Charmander"},
	{5, "Charmeleon"},
	{6, "Charizard"},
	{7, "Squirtle"},
	{8, "Wartortle"},
	{9, "Blastoise"},
	{10, "Caterpie"},
	{11, "Metapod"},
	{12, "Butterfree"},
	{13, "Weedle"},
	{14, "Kakuna"},
	{15, "Beedrill"},
	{16, "Pidgey"},
	{17, "Pidgeotto"},
	{18, "Pidgeot"},
	{19, "Rattata"},
	{20, "Raticate"},
	{21, "Spearow"},
	{22, "Fearow"},
	{23, "Ekans"},
	{24, "Arbok"},
	{25, "Pikachu"},
	{26, "Raichu"},
	{27, "Sandshrew"},
	{28, "Sandslash"},
}

func (p Pokemon) String() string {
	return p.name
}

func main() {
	var pokemon Pokemon

	s := huh.NewSelect[Pokemon]().
		Title("Choose your starter").
		Options(huh.NewOptions(pokemons...)...).
		Value(&pokemon).
		WithHeight(7)

	huh.NewForm(huh.NewGroup(s)).Run()
}


================================================
FILE: examples/readme/select/scroll/scroll.tape
================================================
Output scroll.gif

Set Width 800
Set Padding 40
Set Height 375
Set FontSize 28

Hide
  Type "go build scroll.go"
  Sleep 500ms
  Enter
  Ctrl+L
  Sleep 500ms
  Type "clear && ./scroll"
  Sleep 500ms
  Enter
  Sleep 500ms
Show

Down@250ms 20

Hide
Type "rm scroll" Enter


================================================
FILE: examples/readme/select/select.tape
================================================
Output select.gif

Set Width 1100
Set Padding 40
Set Height 375
Set FontSize 28

Hide
  Type "go build ."
  Sleep 500ms
  Enter
  Ctrl+L
  Sleep 500ms
  Type "clear && ./select"
  Sleep 500ms
  Enter
  Sleep 500ms
Show

Sleep 1s
Down@500ms 3
Sleep 1s
Up@500ms 2
Sleep 1s

Type "/"
Sleep 1s
Type "cana"
Sleep 2s

Hide
Type "rm select" Enter


================================================
FILE: examples/readme/text/main.go
================================================
package main

import "charm.land/huh/v2"

// TODO: ensure input is not plagiarized.
func checkForPlagiarism(s string) error { return nil }

func main() {
	var story string

	text := huh.NewText().
		Title("Tell me a story.").
		Validate(checkForPlagiarism).
		Placeholder("What's on your mind?").
		Value(&story)

	// Create a form to show help.
	form := huh.NewForm(huh.NewGroup(text))
	form.Run()
}


================================================
FILE: examples/readme/text/text.tape
================================================
Output text.gif

Set Width 1000
Set Padding 40
Set Height 450
Set FontSize 28

Hide
  Type "go build ." Enter
  Ctrl+L
  Type "./text" Enter
  Sleep 500ms
Show

Sleep 1s
Type "Once upon a time, in the heart of a lush, enchanted forest, there existed a peculiar village named Charm Dale. This village was unlike any other; its cobblestone streets were lined with houses crafted from the timber of ancient, towering trees that sparkled under the sunlight."
Sleep 4s

Hide
Type "rm text" Enter


================================================
FILE: examples/scroll/main.go
================================================
package main

import "charm.land/huh/v2"

func main() {
	form := huh.NewForm(
		huh.NewGroup(
			huh.NewInput().Title("First"),
			huh.NewInput().Title("Second"),
			huh.NewInput().Title("Third"),
			huh.NewInput().Title("Fourth"),
			huh.NewInput().Title("Fifth"),
			huh.NewInput().Title("Sixth"),
			huh.NewInput().Title("Seventh"),
			huh.NewInput().Title("Eighth"),
			huh.NewInput().Title("Nineth"),
			huh.NewInput().Title("Tenth"),
		),
	).WithHeight(5)
	form.Run()
}


================================================
FILE: examples/skip/main.go
================================================
package main

import (
	"charm.land/huh/v2"
)

func main() {
	f := huh.NewForm(
		huh.NewGroup(
			huh.NewNote().
				Title("Charmburger").
				Description("Welcome to _Charmburger™_."),

			huh.NewSelect[string]().
				Options(huh.NewOptions("Charmburger Classic", "Chickwich", "Fishburger", "Charmpossible™ Burger")...).
				Title("Choose your burger").
				Description("At Charm we truly have a burger for everyone."),

			huh.NewNote().
				Title("🍔"),
		),

		huh.NewGroup(
			huh.NewNote().
				Title("Buy 1 get 1 free").
				Description("Welcome back to _Charmburger™_."),

			huh.NewSelect[string]().
				Options(huh.NewOptions("Charmburger Classic", "Chickwich", "Fishburger", "Charmpossible™ Burger")...).
				Title("Choose your burger").
				Description("At Charm we truly have a burger for everyone."),

			huh.NewNote().
				Title("🍔"),
		),
	)

	f.Run()
}


================================================
FILE: examples/spinner/accessible/main.go
================================================
package main

import (
	"context"
	"log"
	"time"

	"charm.land/huh/v2/spinner"
)

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second/2)
	defer cancel()

	err := spinner.New().
		Context(ctx).
		WithAccessible(true).
		Run()
	if err != nil {
		log.Fatalln(err)
	}
}


================================================
FILE: examples/spinner/context/main.go
================================================
package main

import (
	"context"
	"fmt"
	"time"

	"charm.land/huh/v2/spinner"
)

func main() {
	action := func() { time.Sleep(5 * time.Second) }
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	go action()
	spinner.New().Context(ctx).Run()
	fmt.Println("Done!")
}


================================================
FILE: examples/spinner/context-and-action/main.go
================================================
package main

import (
	"context"
	"fmt"
	"log"
	"math/rand"
	"time"

	"charm.land/huh/v2/spinner"
)

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()

	err := spinner.New().
		Context(ctx).
		Action(func() {
			time.Sleep(time.Minute)
		}).
		WithAccessible(rand.Int()%2 == 0).
		Run()
	if err != nil {
		log.Fatalln(err)
	}
	fmt.Println("Done!")
}


================================================
FILE: examples/spinner/context-and-action-and-error/main.go
================================================
package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"charm.land/huh/v2/spinner"
)

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()

	err := spinner.New().
		Context(ctx).
		ActionWithErr(func(context.Context) error {
			time.Sleep(5 * time.Second)
			return nil
		}).
		WithAccessible(false).
		Run()
	if err != nil {
		log.Fatalln(err)
	}
	fmt.Println("Done!")
}


================================================
FILE: examples/spinner/loading/demo.tape
================================================
Output spinner.gif

Set FontSize 32
Set Height 225
Set Width 800

Hide
Type "go build -o spinner ." Enter
Ctrl+L
Sleep 1s
Show

Type "./spinner"
Sleep 500ms
Enter

Sleep 4s


================================================
FILE: examples/spinner/loading/main.go
================================================
package main

import (
	"fmt"
	"time"

	tea "charm.land/bubbletea/v2"
	"charm.land/huh/v2/spinner"
)

func main() {
	action := func() {
		time.Sleep(1 * time.Second)
	}
	if err := spinner.New().
		Title("Preparing your burger...").
		Action(action).
		WithViewHook(func(v tea.View) tea.View {
			v.ProgressBar = tea.NewProgressBar(tea.ProgressBarIndeterminate, 1)
			return v
		}).
		Run(); err != nil {
		fmt.Println("Failed:", err)
		return
	}
	fmt.Println("Order up!")
}


================================================
FILE: examples/spinner/static/main.go
================================================
package main

import (
	"fmt"

	"charm.land/huh/v2/spinner"
)

func main() {
	_ = spinner.New().Title("Loading").WithAccessible(true).Run()
	fmt.Println("Done!")
}


================================================
FILE: examples/ssh-form/main.go
================================================
package main

import (
	"context"
	"errors"
	"fmt"
	"net"
	"os"
	"os/signal"
	"syscall"
	"time"

	tea "charm.land/bubbletea/v2"
	"charm.land/huh/v2"
	"charm.land/lipgloss/v2"
	"charm.land/log/v2"
	"charm.land/wish/v2"
	"charm.land/wish/v2/activeterm"
	"charm.land/wish/v2/bubbletea"
	"github.com/charmbracelet/ssh"
)

const (
	host = "localhost"
	port = "2222"
)

func main() {
	s, err := wish.NewServer(
		wish.WithAddress(net.JoinHostPort(host, port)),
		wish.WithHostKeyPath(".ssh/id_ed25519"),
		wish.WithMiddleware(
			bubbletea.Middleware(teaHandler),
			activeterm.Middleware(),
		),
	)
	if err != nil {
		log.Error("Could not start server", "error", err)
	}

	done := make(chan os.Signal, 1)
	signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
	log.SetReportTimestamp(false)
	log.Infof("Running form over ssh, connect with:")
	fmt.Printf("\n  ssh %s -p %s\n\n", host, port)
	go func() {
		if err = s.ListenAndServe(); err != nil && !errors.Is(err, ssh.ErrServerClosed) {
			log.Error("Could not start server", "error", err)
			done <- nil
		}
	}()

	<-done
	log.Info("Stopping SSH server")
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()
	if err := s.Shutdown(ctx); err != nil && !errors.Is(err, ssh.ErrServerClosed) {
		log.Error("Could not stop server", "error", err)
	}
}

func customTheme(hasDarkBg bool) *huh.Styles {
	custom := huh.ThemeBase(hasDarkBg)
	custom.Blurred.Title = lipgloss.NewStyle().
		Foreground(lipgloss.Color("#444"))
	custom.Blurred.TextInput.Prompt = lipgloss.NewStyle().
		Foreground(lipgloss.Color("#444"))
	custom.Blurred.TextInput.Text = lipgloss.NewStyle().
		Foreground(lipgloss.Color("#444"))
	custom.Focused.TextInput.Cursor = lipgloss.NewStyle().
		Foreground(lipgloss.Color("#7571F9"))
	custom.Focused.Base = lipgloss.NewStyle().
		Padding(0, 1).
		Border(lipgloss.ThickBorder(), false).
		BorderLeft(true).
		BorderForeground(lipgloss.Color("#7571F9"))
	return custom
}

func teaHandler(s ssh.Session) (tea.Model, []tea.ProgramOption) {
	form := huh.NewForm(
		huh.NewGroup(
			huh.NewInput().Title("Username").Key("username"),
			huh.NewInput().Title("Password").EchoMode(huh.EchoModePassword),
		),
	).WithTheme(huh.ThemeFunc(customTheme))
	style := lipgloss.NewStyle().
		Border(lipgloss.NormalBorder()).
		Padding(1, 2).
		BorderForeground(lipgloss.Color("#444444")).
		Foreground(lipgloss.Color("#7571F9"))
	m := model{form: form, style: style}
	return m, nil
}

type model struct {
	form     *huh.Form
	style    lipgloss.Style
	loggedIn bool
}

func (m model) Init() tea.Cmd { return m.form.Init() }

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	var cmds []tea.Cmd

	if m.form != nil {
		f, cmd := m.form.Update(msg)
		m.form = f.(*huh.Form)
		cmds = append(cmds, cmd)
	}

	m.loggedIn = m.form.State == huh.StateCompleted
	if m.form.State == huh.StateAborted {
		return m, tea.Quit
	}

	switch msg := msg.(type) {
	case tea.KeyPressMsg:
		switch msg.String() {
		case "q", "ctrl+c":
			return m, tea.Quit
		}
	}

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

func (m model) View() tea.View {
	var view tea.View
	view.AltScreen = true

	switch {
	case m.form == nil:
		view.SetContent("Starting...")
	case m.loggedIn:
		view.SetContent(m.style.Render("Welcome, " + m.form.GetString("username") + "!"))
	default:
		view.SetContent(m.form.View())
	}
	return view
}


================================================
FILE: examples/stickers/main.go
================================================
package main

import (
	"charm.land/huh/v2"
)

func main() {
	var (
		name    string
		address string
		country string
		email   string
	)

	huh.NewForm(
		huh.NewGroup(
			huh.NewNote().
				Title("\nStickers pls.").
				Description("Make sure to fill out the address exactly\nas it would appear on a parcel."),
			huh.NewInput().
				Title("Full name").
				Validate(huh.ValidateMinLength(1)).
				Value(&name),
			huh.NewSelect[string]().
				Title("Country ").
				Height(1).
				Value(&country).
				Inline(true).
				Options(countries...),
			huh.NewText().
				Title("Address").
				Lines(2).
				Description("Use your country's postal format.").
				Value(&address),
			huh.NewInput().
				Title("Email").
				Description("Optional: so we can send you updates.").
				Value(&email),
		),
	).Run()
}

var countries = huh.NewOptions(
	// common
	"United States",
	"Canada",
	"Germany",
	"Brazil",
	"Mexico",
	"China",
	"India",

	"Afghanistan",
	"Albania",
	"Algeria",
	"American Samoa",
	"Andorra",
	"Angola",
	"Anguilla",
	"Antarctica",
	"Antigua and Barbuda",
	"Argentina",
	"Armenia",
	"Aruba",
	"Australia",
	"Austria",
	"Azerbaijan",
	"Ã…land Islands",
	"Bahamas",
	"Bahrain",
	"Bangladesh",
	"Barbados",
	"Belarus",
	"Belgium",
	"Belize",
	"Benin",
	"Bermuda",
	"Bhutan",
	"Bolivia",
	"Bosnia and Herzegovina",
	"Botswana",
	"Bouvet Island",
	"British Indian Ocean Territory",
	"British Virgin Islands",
	"Brunei",
	"Bulgaria",
	"Burkina Faso",
	"Burundi",
	"Cambodia",
	"Cameroon",
	"Cape Verde",
	"Caribbean Netherlands",
	"Cayman Islands",
	"Central African Republic",
	"Chad",
	"Chile",
	"Christmas Island",
	"Cocos (Keeling) Islands",
	"Colombia",
	"Comoros",
	"Cook Islands",
	"Costa Rica",
	"Croatia",
	"Cuba",
	"Curaçao",
	"Cyprus",
	"Czechia",
	"DR Congo",
	"Denmark",
	"Djibouti",
	"Dominica",
	"Dominican Republic",
	"Ecuador",
	"Egypt",
	"El Salvador",
	"Equatorial Guinea",
	"Eritrea",
	"Estonia",
	"Eswatini",
	"Ethiopia",
	"Falkland Islands",
	"Faroe Islands",
	"Fiji",
	"Finland",
	"France",
	"French Guiana",
	"French Polynesia",
	"French Southern and Antarctic Lands",
	"Gabon",
	"Gambia",
	"Georgia",
	"Ghana",
	"Gibraltar",
	"Greece",
	"Greenland",
	"Grenada",
	"Guadeloupe",
	"Guam",
	"Guatemala",
	"Guernsey",
	"Guinea",
	"Guinea-Bissau",
	"Guyana",
	"Haiti",
	"Heard Island and McDonald Islands",
	"Honduras",
	"Hong Kong",
	"Hungary",
	"Iceland",
	"Indonesia",
	"Iran",
	"Iraq",
	"Ireland",
	"Isle of Man",
	"Israel",
	"Italy",
	"Ivory Coast",
	"Jamaica",
	"Japan",
	"Jersey",
	"Jordan",
	"Kazakhstan",
	"Kenya",
	"Kiribati",
	"Kosovo",
	"Kuwait",
	"Kyrgyzstan",
	"Laos",
	"Latvia",
	"Lebanon",
	"Lesotho",
	"Liberia",
	"Libya",
	"Liechtenstein",
	"Lithuania",
	"Luxembourg",
	"Macau",
	"Madagascar",
	"Malawi",
	"Malaysia",
	"Maldives",
	"Mali",
	"Malta",
	"Marshall Islands",
	"Martinique",
	"Mauritania",
	"Mauritius",
	"Mayotte",
	"Micronesia",
	"Moldova",
	"Monaco",
	"Mongolia",
	"Montenegro",
	"Montserrat",
	"Morocco",
	"Mozambique",
	"Myanmar",
	"Namibia",
	"Nauru",
	"Nepal",
	"Netherlands",
	"New Caledonia",
	"New Zealand",
	"Nicaragua",
	"Niger",
	"Nigeria",
	"Niue",
	"Norfolk Island",
	"North Korea",
	"North Macedonia",
	"Northern Mariana Islands",
	"Norway",
	"Oman",
	"Pakistan",
	"Palau",
	"Palestine",
	"Panama",
	"Papua New Guinea",
	"Paraguay",
	"Peru",
	"Philippines",
	"Pitcairn Islands",
	"Poland",
	"Portugal",
	"Puerto Rico",
	"Qatar",
	"Republic of the Congo",
	"Romania",
	"Russia",
	"Rwanda",
	"São Tomé and Príncipe",
	"Saint Barthélemy",
	"Saint Helena, Ascension and Tristan da Cunha",
	"Saint Kitts and Nevis",
	"Saint Lucia",
	"Saint Martin",
	"Saint Pierre and Miquelon",
	"Saint Vincent and the Grenadines",
	"Samoa",
	"San Marino",
	"Saudi Arabia",
	"Senegal",
	"Serbia",
	"Seychelles",
	"Sierra Leone",
	"Singapore",
	"Sint Maarten",
	"Slovakia",
	"Slovenia",
	"Solomon Islands",
	"Somalia",
	"South Africa",
	"South Georgia",
	"South Korea",
	"South Sudan",
	"Spain",
	"Sri Lanka",
	"Sudan",
	"Suriname",
	"Svalbard and Jan Mayen",
	"Sweden",
	"Switzerland",
	"Syria",
	"Taiwan",
	"Tajikistan",
	"Tanzania",
	"Thailand",
	"Timor-Leste",
	"Togo",
	"Tokelau",
	"Tonga",
	"Trinidad and Tobago",
	"Tunisia",
	"Turkey",
	"Turkmenistan",
	"Turks and Caicos Islands",
	"Tuvalu",
	"Uganda",
	"Ukraine",
	"United Arab Emirates",
	"United Kingdom",
	"United States Minor Outlying Islands",
	"United States Virgin Islands",
	"Uruguay",
	"Uzbekistan",
	"Vanuatu",
	"Vatican City",
	"Venezuela",
	"Vietnam",
	"Wallis and Futuna",
	"Western Sahara",
	"Yemen",
	"Zambia",
	"Zimbabwe",
)


================================================
FILE: examples/theme/main.go
================================================
package main

import (
	"fmt"
	"os"

	"charm.land/huh/v2"
)

var themes = map[string]huh.Theme{
	"default":    huh.ThemeFunc(huh.ThemeBase),
	"dracula":    huh.ThemeFunc(huh.ThemeDracula),
	"base16":     huh.ThemeFunc(huh.ThemeBase16),
	"charm":      huh.ThemeFunc(huh.ThemeCharm),
	"catppuccin": huh.ThemeFunc(huh.ThemeCatppuccin),
}

func main() {
	theme := "base16"
	repeat := true

	for {
		err := huh.NewSelect[string]().
			Title("Theme").
			Value(&theme).
			Options(
				huh.NewOption("Default", "default"),
				huh.NewOption("Dracula", "dracula"),
				huh.NewOption("Base 16", "base16"),
				huh.NewOption("Charm", "charm"),
				huh.NewOption("Catppuccin", "catppuccin"),
				huh.NewOption("Exit", ""),
			).Run()
		if err != nil {
			if err == huh.ErrUserAborted {
				os.Exit(130)
			}
			fmt.Println(err)
			os.Exit(1)
		}
		if theme == "" {
			break
		}

		// Display form with selected theme.
		err = huh.NewForm(
			huh.NewGroup(
				huh.NewInput().Title("Thoughts").Placeholder("What's on your mind?"),
				huh.NewSelect[string]().Options(huh.NewOptions("A", "B", "C")...).Title("Colors"),
				huh.NewFilePicker().Title("File"),
				huh.NewMultiSelect[string]().Options(huh.NewOptions("Red", "Green", "Yellow")...).Title("Letters"),
				huh.NewConfirm().Title("Again?").Description("Try another theme").Value(&repeat),
			),
		).WithTheme(themes[theme]).Run()
		if err != nil {
			if err == huh.ErrUserAborted {
				os.Exit(130)
			}
			fmt.Println(err)
			os.Exit(1)
		}

		if !repeat {
			break
		}
	}
}


================================================
FILE: examples/theme/theme.tape
================================================
Output theme.gif

Set Width 800
Set Height 740
Set Padding 80

Hide
Type "go build -o theme ."
Enter
Ctrl+L
Sleep 500ms
Type "clear && ./theme" Enter
Show

Sleep 2s

Enter

Screenshot default-theme.png

Tab 4
Down 1
Enter

Screenshot dracula-theme.png

Tab 4
Down 2
Enter

Screenshot basesixteen-theme.png

Tab 4
Down 3
Enter

Screenshot charm-theme.png

Tab 4
Down 4
Enter

Screenshot catppuccin-theme.png

Sleep 1s

Hide
Type "rm theme"
Enter


================================================
FILE: examples/timer/main.go
================================================
package main

import (
	"log"
	"strings"
	"time"

	"charm.land/bubbles/v2/progress"
	tea "charm.land/bubbletea/v2"
	"charm.land/huh/v2"
	"charm.land/lipgloss/v2"
)

var (
	focusColor = lipgloss.Color("#2EF8BB")
	breakColor = lipgloss.Color("#FF5F87")
)

var (
	focusTitleStyle = lipgloss.NewStyle().Foreground(focusColor).MarginRight(1).SetString("Focus Mode")
	breakTitleStyle = lipgloss.NewStyle().Foreground(breakColor).MarginRight(1).SetString("Break Mode")
	pausedStyle     = lipgloss.NewStyle().Foreground(breakColor).MarginRight(1).SetString("Continue?")
	helpStyle       = lipgloss.NewStyle().Foreground(lipgloss.Color("240")).MarginTop(2)
	sidebarStyle    = lipgloss.NewStyle().MarginLeft(3).Padding(1, 3).Border(lipgloss.RoundedBorder()).BorderForeground(helpStyle.GetForeground())
)

var baseTimerStyle = lipgloss.NewStyle().Padding(1, 2)

type mode int

const (
	Initial mode = iota
	Focusing
	Paused
	Breaking
)

type Model struct {
	form     *huh.Form
	quitting bool

	lastTick  time.Time
	startTime time.Time

	mode mode

	focusTime time.Duration
	breakTime time.Duration

	progress progress.Model
}

func (m Model) Init() tea.Cmd {
	return m.form.Init()
}

const tickInterval = time.Second / 2

type tickMsg time.Time

func tickCmd(t time.Time) tea.Msg {
	return tickMsg(t)
}

func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	var cmds []tea.Cmd

	switch msg := msg.(type) {
	case tickMsg:
		cmds = append(cmds, tea.Tick(tickInterval, tickCmd))
	case tea.KeyPressMsg:
		switch msg.String() {
		case "q":
			switch m.mode {
			case Focusing:
				m.mode = Paused
				m.startTime = time.Now()
				m.progress.FullColor = breakColor
			case Paused:
				m.mode = Breaking
				m.startTime = time.Now()
			case Breaking:
				m.quitting = true
				return m, tea.Quit
			}
		case "ctrl+c":
			m.quitting = true
			return m, tea.Interrupt
		default:
			if m.mode == Paused {
				m.mode = Breaking
				m.startTime = time.Now()
			}
		}
	}

	// Update form
	f, cmd := m.form.Update(msg)
	m.form = f.(*huh.Form)
	cmds = append(cmds, cmd)
	if m.form.State != huh.StateCompleted {
		return m, tea.Batch(cmds...)
	}

	// Update timer
	if m.startTime.IsZero() {
		m.startTime = time.Now()
		m.focusTime = m.form.Get("focus").(time.Duration)
		m.breakTime = m.form.Get("break").(time.Duration)
		m.mode = Focusing
		cmds = append(cmds, tea.Tick(tickInterval, tickCmd))
	}

	switch m.mode {
	case Focusing:
		if time.Now().After(m.startTime.Add(m.focusTime)) {
			m.mode = Paused
			m.startTime = time.Now()
			m.progress.FullColor = breakColor
		}
	case Breaking:
		if time.Now().After(m.startTime.Add(m.breakTime)) {
			m.quitting = true
			return m, tea.Quit
		}
	}

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

func (m Model) View() tea.View {
	if m.quitting {
		return tea.NewView("")
	}

	if m.form.State != huh.StateCompleted {
		return tea.NewView(m.form.View())
	}

	var s strings.Builder

	elapsed := time.Since(m.startTime)
	var percent float64
	switch m.mode {
	case Focusing:
		percent = float64(elapsed) / float64(m.focusTime)
		s.WriteString(focusTitleStyle.String())
		s.WriteString(elapsed.Round(time.Second).String())
		s.WriteString("\n\n")
		s.WriteString(m.progress.ViewAs(percent))
		s.WriteString(helpStyle.Render("Press 'q' to skip"))
	case Paused:
		s.WriteString(pausedStyle.String())
		s.WriteString("\n\nFocus time is done, time to take a break.")
		s.WriteString(helpStyle.Render("press any key to continue.\n"))
	case Breaking:
		percent = float64(elapsed) / float64(m.breakTime)
		s.WriteString(breakTitleStyle.String())
		s.WriteString(elapsed.Round(time.Second).String())
		s.WriteString("\n\n")
		s.WriteString(m.progress.ViewAs(percent))
		s.WriteString(helpStyle.Render("press 'q' to quit"))
	}

	return tea.NewView(baseTimerStyle.Render(s.String()))
}

func customTheme(isDark bool) *huh.Styles {
	theme := huh.ThemeCharm(isDark)
	theme.Focused.Base.Border(lipgloss.HiddenBorder())
	theme.Focused.Title.Foreground(focusColor)
	theme.Focused.SelectSelector.Foreground(focusColor)
	theme.Focused.SelectedOption.Foreground(lipgloss.Color("15"))
	theme.Focused.Option.Foreground(lipgloss.Color("7"))
	return theme
}

func NewModel() Model {
	form := huh.NewForm(
		huh.NewGroup(
			huh.NewSelect[time.Duration]().
				Title("Focus Time").
				Key("focus").
				Options(
					huh.NewOption("25 minutes", 25*time.Minute),
					huh.NewOption("30 minutes", 30*time.Minute),
					huh.NewOption("45 minutes", 45*time.Minute),
					huh.NewOption("1 hour", time.Hour),
				),
		),
		huh.NewGroup(
			huh.NewSelect[time.Duration]().
				Title("Break Time").
				Key("break").
				Options(
					huh.NewOption("5 minutes", 5*time.Minute),
					huh.NewOption("10 minutes", 10*time.Minute),
					huh.NewOption("15 minutes", 15*time.Minute),
					huh.NewOption("20 minutes", 20*time.Minute),
				),
		),
	).WithShowHelp(false).WithTheme(huh.ThemeFunc(customTheme))

	progress := progress.New()
	progress.FullColor = focusColor
	progress.SetSpringOptions(1, 1)

	return Model{
		form:     form,
		progress: progress,
	}
}

func main() {
	m := NewModel()
	mm, err := tea.NewProgram(&m).Run()
	m = mm.(Model)
	if err != nil {
		log.Fatal(err)
	}
}


================================================
FILE: field_confirm.go
================================================
package huh

import (
	"cmp"
	"io"
	"strings"

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

// Confirm is a form confirm field.
type Confirm struct {
	accessor Accessor[bool]
	key      string
	id       int

	// customization
	title       Eval[string]
	description Eval[string]
	affirmative string
	negative    string

	// error handling
	validate func(bool) error
	err      error

	// state
	focused bool

	// options
	width           int
	height          int
	inline          bool
	theme           Theme
	hasDarkBg       bool
	keymap          ConfirmKeyMap
	buttonAlignment lipgloss.Position
}

// NewConfirm returns a new confirm field.
func NewConfirm() *Confirm {
	return &Confirm{
		accessor:        &EmbeddedAccessor[bool]{},
		id:              nextID(),
		title:           Eval[string]{cache: make(map[uint64]string)},
		description:     Eval[string]{cache: make(map[uint64]string)},
		affirmative:     "Yes",
		negative:        "No",
		validate:        func(bool) error { return nil },
		buttonAlignment: lipgloss.Center,
	}
}

// Validate sets the validation function of the confirm field.
func (c *Confirm) Validate(validate func(bool) error) *Confirm {
	c.validate = validate
	return c
}

// Error returns the error of the confirm field.
func (c *Confirm) Error() error {
	return c.err
}

// Skip returns whether the confirm should be skipped or should be blocking.
func (*Confirm) Skip() bool {
	return false
}

// Zoom returns whether the input should be zoomed.
func (*Confirm) Zoom() bool {
	return false
}

// Affirmative sets the affirmative value of the confirm field.
func (c *Confirm) Affirmative(affirmative string) *Confirm {
	c.affirmative = affirmative
	return c
}

// Negative sets the negative value of the confirm field.
func (c *Confirm) Negative(negative string) *Confirm {
	c.negative = negative
	return c
}

// Value sets the value of the confirm field.
func (c *Confirm) Value(value *bool) *Confirm {
	return c.Accessor(NewPointerAccessor(value))
}

// Accessor sets the accessor of the confirm field.
func (c *Confirm) Accessor(accessor Accessor[bool]) *Confirm {
	c.accessor = accessor
	return c
}

// Key sets the key of the confirm field.
func (c *Confirm) Key(key string) *Confirm {
	c.key = key
	return c
}

// Title sets the title of the confirm field.
func (c *Confirm) Title(title string) *Confirm {
	c.title.val = title
	c.title.fn = nil
	return c
}

// TitleFunc sets the title func of the confirm field.
func (c *Confirm) TitleFunc(f func() string, bindings any) *Confirm {
	c.title.fn = f
	c.title.bindings = bindings
	return c
}

// Description sets the description of the confirm field.
func (c *Confirm) Description(description string) *Confirm {
	c.description.val = description
	c.description.fn = nil
	return c
}

// DescriptionFunc sets the description function of the confirm field.
func (c *Confirm) DescriptionFunc(f func() string, bindings any) *Confirm {
	c.description.fn = f
	c.description.bindings = bindings
	return c
}

// Inline sets whether the field should be inline.
func (c *Confirm) Inline(inline bool) *Confirm {
	c.inline = inline
	return c
}

// Focus focuses the confirm field.
func (c *Confirm) Focus() tea.Cmd {
	c.focused = true
	return nil
}

// Blur blurs the confirm field.
func (c *Confirm) Blur() tea.Cmd {
	c.focused = false
	c.err = c.validate(c.accessor.Get())
	return nil
}

// KeyBinds returns the help message for the confirm field.
func (c *Confirm) KeyBinds() []key.Binding {
	return []key.Binding{c.keymap.Toggle, c.keymap.Prev, c.keymap.Submit, c.keymap.Next, c.keymap.Accept, c.keymap.Reject}
}

// Init initializes the confirm field.
func (c *Confirm) Init() tea.Cmd {
	return nil
}

// Update updates the confirm field.
func (c *Confirm) Update(msg tea.Msg) (Model, tea.Cmd) {
	var cmds []tea.Cmd

	switch msg := msg.(type) {
	case tea.BackgroundColorMsg:
		c.hasDarkBg = msg.IsDark()
	case updateFieldMsg:
		if ok, hash := c.title.shouldUpdate(); ok {
			c.title.bindingsHash = hash
			if !c.title.loadFromCache() {
				c.title.loading = true
				cmds = append(cmds, func() tea.Msg {
					return updateTitleMsg{id: c.id, title: c.title.fn(), hash: hash}
				})
			}
		}
		if ok, hash := c.description.shouldUpdate(); ok {
			c.description.bindingsHash = hash
			if !c.description.loadFromCache() {
				c.description.loading = true
				cmds = append(cmds, func() tea.Msg {
					return updateDescriptionMsg{id: c.id, description: c.description.fn(), hash: hash}
				})
			}
		}

	case updateTitleMsg:
		if msg.id == c.id && msg.hash == c.title.bindingsHash {
			c.title.val = msg.title
			c.title.loading = false
		}
	case updateDescriptionMsg:
		if msg.id == c.id && msg.hash == c.description.bindingsHash {
			c.description.val = msg.description
			c.description.loading = false
		}
	case tea.KeyPressMsg:
		c.err = nil
		switch {
		case key.Matches(msg, c.keymap.Toggle):
			if c.negative == "" {
				break
			}
			c.accessor.Set(!c.accessor.Get())
		case key.Matches(msg, c.keymap.Prev):
			cmds = append(cmds, PrevField)
		case key.Matches(msg, c.keymap.Next, c.keymap.Submit):
			cmds = append(cmds, NextField)
		case key.Matches(msg, c.keymap.Accept):
			c.accessor.Set(true)
			cmds = append(cmds, NextField)
		case key.Matches(msg, c.keymap.Reject):
			c.accessor.Set(false)
			cmds = append(cmds, NextField)
		}
	}

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

func (c *Confirm) activeStyles() *FieldStyles {
	theme := c.theme
	if theme == nil {
		theme = ThemeFunc(ThemeCharm)
	}
	if c.focused {
		return &theme.Theme(c.hasDarkBg).Focused
	}
	return &theme.Theme(c.hasDarkBg).Blurred
}

// View renders the confirm field.
func (c *Confirm) View() string {
	styles := c.activeStyles()
	maxWidth := c.width - styles.Base.GetHorizontalFrameSize()

	var wroteHeader bool
	var sb strings.Builder
	if c.title.val != "" {
		sb.WriteString(styles.Title.Render(wrap(c.title.val, maxWidth)))
		wroteHeader = true
	}
	if c.err != nil {
		sb.WriteString(styles.ErrorIndicator.String())
		wroteHeader = true
	}

	if c.description.val != "" {
		description := styles.Description.Render(wrap(c.description.val, maxWidth))
		if !c.inline && (c.description.val != "" || c.description.fn != nil) {
			sb.WriteString("\n")
		}
		sb.WriteString(description)
		wroteHeader = true
	}

	if !c.inline && wroteHeader {
		sb.WriteString("\n")
		sb.WriteString("\n")
	}

	var negative string
	var affirmative string
	if c.negative != "" {
		if c.accessor.Get() {
			affirmative = styles.FocusedButton.Render(c.affirmative)
			negative = styles.BlurredButton.Render(c.negative)
		} else {
			affirmative = styles.BlurredButton.Render(c.affirmative)
			negative = styles.FocusedButton.Render(c.negative)
		}
		c.keymap.Reject.SetHelp("n", c.negative)
	} else {
		affirmative = styles.FocusedButton.Render(c.affirmative)
		c.keymap.Reject.SetEnabled(false)
	}

	c.keymap.Accept.SetHelp("y", c.affirmative)

	buttonsRow := lipgloss.JoinHorizontal(c.buttonAlignment, affirmative, negative)

	promptWidth := lipgloss.Width(sb.String())
	buttonsWidth := lipgloss.Width(buttonsRow)

	renderWidth := max(buttonsWidth, promptWidth)

	style := lipgloss.NewStyle().Width(renderWidth).Align(c.buttonAlignment)

	sb.WriteString(style.Render(buttonsRow))
	return styles.Base.Width(c.width).Height(c.height).
		Render(sb.String())
}

// Run runs the confirm field in accessible mode.
func (c *Confirm) Run() error {
	return Run(c)
}

// RunAccessible runs the confirm field in accessible mode.
func (c *Confirm) RunAccessible(w io.Writer, r io.Reader) error {
	styles := c.activeStyles()
	defaultValue := c.GetValue().(bool)
	opts := "[y/N]"
	if defaultValue {
		opts = "[Y/n]"
	}
	prompt := styles.Title.
		PaddingRight(1).
		Render(cmp.Or(c.title.val, "Choose"), opts)
	c.accessor.Set(accessibility.PromptBool(w, r, prompt, defaultValue))
	return nil
}

func (c *Confirm) String() string {
	if c.accessor.Get() {
		return c.affirmative
	}
	return c.negative
}

// WithTheme sets the theme of the confirm field.
func (c *Confirm) WithTheme(theme Theme) Field {
	if c.theme != nil {
		return c
	}
	c.theme = theme
	return c
}

// WithKeyMap sets the keymap of the confirm field.
func (c *Confirm) WithKeyMap(k *KeyMap) Field {
	c.keymap = k.Confirm
	return c
}

// WithWidth sets the width of the confirm field.
func (c *Confirm) WithWidth(width int) Field {
	c.width = width
	return c
}

// WithHeight sets the height of the confirm field.
func (c *Confirm) WithHeight(height int) Field {
	c.height = height
	return c
}

// WithPosition sets the position of the confirm field.
func (c *Confirm) WithPosition(p FieldPosition) Field {
	c.keymap.Prev.SetEnabled(!p.IsFirst())
	c.keymap.Next.SetEnabled(!p.IsLast())
	c.keymap.Submit.SetEnabled(p.IsLast())
	return c
}

// WithButtonAlignment sets the button position of the confirm field.
func (c *Confirm) WithButtonAlignment(p lipgloss.Position) *Confirm {
	c.buttonAlignment = p
	return c
}

// GetKey returns the key of the field.
func (c *Confirm) GetKey() string {
	return c.key
}

// GetValue returns the value of the field.
func (c *Confirm) GetValue() any {
	return c.accessor.Get()
}


================================================
FILE: field_filepicker.go
================================================
package huh

import (
	"cmp"
	"errors"
	"io"
	"os"
	"strings"

	xstrings "github.com/charmbracelet/x/exp/strings"

	"charm.land/bubbles/v2/filepicker"
	"charm.land/bubbles/v2/key"
	tea "charm.land/bubbletea/v2"
	"charm.land/huh/v2/internal/accessibility"
	"charm.land/lipgloss/v2"
)

// FilePicker is a form file file field.
type FilePicker struct {
	accessor Accessor[string]
	key      string
	picker   filepicker.Model

	// state
	focused bool
	picking bool

	// customization
	title       string
	description string

	// error handling
	validate func(string) error
	err      error

	// options
	width     int
	height    int
	theme     Theme
	hasDarkBg bool
	keymap    FilePickerKeyMap
}

// NewFilePicker returns a new file field.
func NewFilePicker() *FilePicker {
	fp := filepicker.New()
	fp.ShowSize = false

	if cmd := fp.Init(); cmd != nil {
		fp, _ = fp.Update(cmd())
	}

	return &FilePicker{
		accessor: &EmbeddedAccessor[string]{},
		validate: func(string) error { return nil },
		picker:   fp,
	}
}

// CurrentDirectory sets the directory of the file field.
func (f *FilePicker) CurrentDirectory(directory string) *FilePicker {
	f.picker.CurrentDirectory = directory
	if cmd := f.picker.Init(); cmd != nil {
		f.picker, _ = f.picker.Update(cmd())
	}
	return f
}

// Cursor sets the cursor of the file field.
func (f *FilePicker) Cursor(cursor string) *FilePicker {
	f.picker.Cursor = cursor
	return f
}

// Picking sets whether the file picker should be in the picking files state.
func (f *FilePicker) Picking(v bool) *FilePicker {
	f.setPicking(v)
	return f
}

// ShowHidden sets whether to show hidden files.
func (f *FilePicker) ShowHidden(v bool) *FilePicker {
	f.picker.ShowHidden = v
	return f
}

// ShowSize sets whether to show file sizes.
func (f *FilePicker) ShowSize(v bool) *FilePicker {
	f.picker.ShowSize = v
	return f
}

// ShowPermissions sets whether to show file permissions.
func (f *FilePicker) ShowPermissions(v bool) *FilePicker {
	f.picker.ShowPermissions = v
	return f
}

// FileAllowed sets whether to allow files to be selected.
func (f *FilePicker) FileAllowed(v bool) *FilePicker {
	f.picker.FileAllowed = v
	return f
}

// DirAllowed sets whether to allow directories to be selected.
func (f *FilePicker) DirAllowed(v bool) *FilePicker {
	f.picker.DirAllowed = v
	return f
}

// Value sets the value of the file field.
func (f *FilePicker) Value(value *string) *FilePicker {
	return f.Accessor(NewPointerAccessor(value))
}

// Accessor sets the accessor of the file field.
func (f *FilePicker) Accessor(accessor Accessor[string]) *FilePicker {
	f.accessor = accessor
	return f
}

// Key sets the key of the file field which can be used to retrieve the value
// after submission.
func (f *FilePicker) Key(key string) *FilePicker {
	f.key = key
	return f
}

// Title sets the title of the file field.
func (f *FilePicker) Title(title string) *FilePicker {
	f.title = title
	return f
}

// Description sets the description of the file field.
func (f *FilePicker) Description(description string) *FilePicker {
	f.description = description
	return f
}

// AllowedTypes sets the allowed types of the file field. These will be the only
// valid file types accepted, other files will show as disabled.
func (f *FilePicker) AllowedTypes(types []string) *FilePicker {
	f.picker.AllowedTypes = types
	return f
}

// Height sets the height of the file field. If the number of options
// exceeds the height, the file field will become scrollable.
func (f *FilePicker) Height(height int) *FilePicker {
	f.WithHeight(height)
	return f
}

// Validate sets the validation function of the file field.
func (f *FilePicker) Validate(validate func(string) error) *FilePicker {
	f.validate = validate
	return f
}

// Error returns the error of the file field.
func (f *FilePicker) Error() error {
	return f.err
}

// Skip returns whether the file should be skipped or should be blocking.
func (*FilePicker) Skip() bool {
	return false
}

// Zoom returns whether the input should be zoomed.
func (f *FilePicker) Zoom() bool {
	return f.picking
}

// Focus focuses the file field.
func (f *FilePicker) Focus() tea.Cmd {
	f.focused = true
	return f.picker.Init()
}

// Blur blurs the file field.
func (f *FilePicker) Blur() tea.Cmd {
	f.focused = false
	f.setPicking(false)
	f.err = f.validate(f.accessor.Get())
	return nil
}

// KeyBinds returns the help keybindings for the file field.
func (f *FilePicker) KeyBinds() []key.Binding {
	return []key.Binding{f.keymap.Up, f.keymap.Down, f.keymap.Close, f.keymap.Open, f.keymap.Prev, f.keymap.Next, f.keymap.Submit}
}

// Init initializes the file field.
func (f *FilePicker) Init() tea.Cmd {
	return f.picker.Init()
}

// Update updates the file field.
func (f *FilePicker) Update(msg tea.Msg) (Model, tea.Cmd) {
	f.err = nil

	switch msg := msg.(type) {
	case tea.BackgroundColorMsg:
		f.hasDarkBg = msg.IsDark()
	case tea.KeyPressMsg:
		switch {
		case key.Matches(msg, f.keymap.Open):
			if f.picking {
				break
			}
			f.setPicking(true)
			return f, f.picker.Init()
		case key.Matches(msg, f.keymap.Close):
			f.setPicking(false)
			return f, NextField
		case key.Matches(msg, f.keymap.Next):
			f.setPicking(false)
			return f, NextField
		case key.Matches(msg, f.keymap.Prev):
			f.setPicking(false)
			return f, PrevField
		}
	}

	var cmd tea.Cmd
	f.picker, cmd = f.picker.Update(msg)
	didSelect, file := f.picker.DidSelectFile(msg)
	if didSelect {
		f.accessor.Set(file)
		f.setPicking(false)
		return f, NextField
	}
	didSelect, _ = f.picker.DidSelectDisabledFile(msg)
	if didSelect {
		f.err = errors.New(xstrings.EnglishJoin(f.picker.AllowedTypes, true) + " files only")
		return f, nil
	}

	return f, cmd
}

func (f *FilePicker) activeStyles() *FieldStyles {
	theme := f.theme
	if theme == nil {
		theme = ThemeFunc(ThemeCharm)
	}
	if f.focused {
		return &theme.Theme(f.hasDarkBg).Focused
	}
	return &theme.Theme(f.hasDarkBg).Blurred
}

func (f *FilePicker) renderTitle() string {
	styles := f.activeStyles()
	maxWidth := f.width - styles.Base.GetHorizontalFrameSize()
	return styles.Title.Render(wrap(f.title, maxWidth))
}

func (f FilePicker) renderDescription() string {
	styles := f.activeStyles()
	maxWidth := f.width - styles.Base.GetHorizontalFrameSize()
	return styles.Description.Render(wrap(f.description, maxWidth))
}

// View renders the file field.
func (f *FilePicker) View() string {
	styles := f.activeStyles()
	var parts []string
	if f.title != "" {
		parts = append(parts, f.renderTitle())
	}
	if f.description != "" {
		parts = append(parts, f.renderDescription())
	}
	parts = append(parts, f.pickerView())
	return styles.Base.Width(f.width).Height(f.height).
		Render(strings.Join(parts, "\n"))
}

func (f *FilePicker) pickerView() string {
	if f.picking {
		return f.picker.View()
	}
	styles := f.activeStyles()
	if f.accessor.Get() != "" {
		return styles.SelectedOption.Render(f.accessor.Get())
	}
	return styles.TextInput.Placeholder.Render("No file selected.")
}

func (f *FilePicker) setPicking(v bool) {
	f.picking = v

	f.keymap.Close.SetEnabled(v)
	f.keymap.Up.SetEnabled(v)
	f.keymap.Down.SetEnabled(v)
	f.keymap.Select.SetEnabled(v)
	f.keymap.Back.SetEnabled(v)

	f.picker.KeyMap.Up.SetEnabled(v)
	f.picker.KeyMap.Down.SetEnabled(v)
	f.picker.KeyMap.GoToTop.SetEnabled(v)
	f.picker.KeyMap.GoToLast.SetEnabled(v)
	f.picker.KeyMap.Select.SetEnabled(v)
	f.picker.KeyMap.Open.SetEnabled(v)
	f.picker.KeyMap.Back.SetEnabled(v)
}

// Run runs the file field.
func (f *FilePicker) Run() error {
	return Run(f)
}

// RunAccessible runs an accessible file field.
func (f *FilePicker) RunAccessible(w io.Writer, r io.Reader) error {
	styles := f.activeStyles()
	prompt := styles.Title.
		PaddingRight(1).
		Render(cmp.Or(f.title, "Choose a file:"))

	validateFile := func(s string) error {
		// is the string a file?
		if _, err := os.Open(s); err != nil {
			return errors.New("not a file")
		}

		// is it one of the allowed types?
		valid := len(f.picker.AllowedTypes) == 0
		for _, ext := range f.picker.AllowedTypes {
			if strings.HasSuffix(s, ext) {
				valid = true
				break
			}
		}
		if !valid {
			return errors.New("cannot select: " + s)
		}

		// does it pass user validation?
		return f.validate(s)
	}

	f.accessor.Set(accessibility.PromptString(
		w,
		r,
		prompt,
		f.GetValue().(string),
		validateFile,
	))
	return nil
}

// copied from bubbles' filepicker.
const (
	fileSizeWidth = 7
	paddingLeft   = 2
)

// WithTheme sets the theme of the file field.
func (f *FilePicker) WithTheme(theme Theme) Field {
	if f.theme != nil || theme == nil {
		return f
	}
	f.theme = theme
	styles := f.theme.Theme(f.hasDarkBg)

	// XXX: add specific themes
	f.picker.Styles = filepicker.Styles{
		DisabledCursor:   lipgloss.Style{},
		Cursor:           styles.Focused.TextInput.Prompt,
		Symlink:          lipgloss.NewStyle(),
		Directory:        styles.Focused.Directory,
		File:             styles.Focused.File,
		DisabledFile:     styles.Focused.TextInput.Placeholder,
		Permission:       styles.Focused.TextInput.Placeholder,
		Selected:         styles.Focused.SelectedOption,
		DisabledSelected: styles.Focused.TextInput.Placeholder,
		FileSize:         styles.Focused.TextInput.Placeholder.Width(fileSizeWidth).Align(lipgloss.Right),
		EmptyDirectory:   styles.Focused.TextInput.Placeholder.PaddingLeft(paddingLeft).SetString("No files found."),
	}

	return f
}

// WithKeyMap sets the keymap on a file field.
func (f *FilePicker) WithKeyMap(k *KeyMap) Field {
	f.keymap = k.FilePicker
	f.picker.KeyMap = filepicker.KeyMap{
		GoToTop:  k.FilePicker.GotoTop,
		GoToLast: k.FilePicker.GotoBottom,
		Down:     k.FilePicker.Down,
		Up:       k.FilePicker.Up,
		PageUp:   k.FilePicker.PageUp,
		PageDown: k.FilePicker.PageDown,
		Back:     k.FilePicker.Back,
		Open:     k.FilePicker.Open,
		Select:   k.FilePicker.Select,
	}
	f.setPicking(f.picking)
	return f
}

// WithWidth sets the width of the file field.
func (f *FilePicker) WithWidth(width int) Field {
	f.width = width
	return f
}

// WithHeight sets the height of the file field.
func (f *FilePicker) WithHeight(height int) Field {
	if height == 0 {
		return f
	}
	adjust := 0
	if f.title != "" {
		adjust += lipgloss.Height(f.renderTitle())
	}
	if f.description != "" {
		adjust += lipgloss.Height(f.renderDescription())
	}
	adjust++ // picker's own help height
	f.picker.SetHeight(height - adjust)
	return f
}

// WithPosition sets the position of the file field.
func (f *FilePicker) WithPosition(p FieldPosition) Field {
	f.keymap.Prev.SetEnabled(!p.IsFirst())
	f.keymap.Next.SetEnabled(!p.IsLast())
	f.keymap.Submit.SetEnabled(p.IsLast())
	return f
}

// GetKey returns the key of the field.
func (f *FilePicker) GetKey() string {
	return f.key
}

// GetValue returns the value of the field.
func (f *FilePicker) GetValue() any {
	return f.accessor.Get()
}


================================================
FILE: field_input.go
================================================
package huh

import (
	"cmp"
	"errors"
	"fmt"
	"io"
	"strings"

	"charm.land/bubbles/v2/key"
	"charm.land/bubbles/v2/textinput"
	tea "charm.land/bubbletea/v2"
	"charm.land/huh/v2/internal/accessibility"
	"charm.land/lipgloss/v2"
)

// Input is a input field.
//
// The input field is a field that allows the user to enter text. Use it to user
// input. It can be used for collecting text, passwords, or other short input.
//
// The input field supports Suggestions, Placeholder, and Validation.
type Input struct {
	accessor Accessor[string]
	key      string
	id       int

	title       Eval[string]
	description Eval[string]
	placeholder Eval[string]
	suggestions Eval[[]string]

	textinput textinput.Model

	inline   bool
	validate func(string) error
	err      error
	focused  bool

	width  int
	height int

	theme     Theme
	hasDarkBg bool
	keymap    InputKeyMap
}

// NewInput creates a new input field.
//
// The input field is a field that allows the user to enter text. Use it to user
// input. It can be used for collecting text, passwords, or other short input.
//
// The input field supports Suggestions, Placeholder, and Validation.
func NewInput() *Input {
	input := textinput.New()

	i := &Input{
		accessor:    &EmbeddedAccessor[string]{},
		textinput:   input,
		validate:    func(string) error { return nil },
		id:          nextID(),
		title:       Eval[string]{cache: make(map[uint64]string)},
		description: Eval[string]{cache: make(map[uint64]string)},
		placeholder: Eval[string]{cache: make(map[uint64]string)},
		suggestions: Eval[[]string]{cache: make(map[uint64][]string)},
	}

	return i
}

// Value sets the value of the input field.
func (i *Input) Value(value *string) *Input {
	return i.Accessor(NewPointerAccessor(value))
}

// Accessor sets the accessor of the input field.
func (i *Input) Accessor(accessor Accessor[string]) *Input {
	i.accessor = accessor
	i.textinput.SetValue(i.accessor.Get())
	return i
}

// Key sets the key of the input field.
func (i *Input) Key(key string) *Input {
	i.key = key
	return i
}

// Title sets the title of the input field.
//
// The Title is static for dynamic Title use `TitleFunc`.
func (i *Input) Title(title string) *Input {
	i.title.val = title
	i.title.fn = nil
	return i
}

// Description sets the description of the input field.
//
// The Description is static for dynamic Description use `DescriptionFunc`.
func (i *Input) Description(description string) *Input {
	i.description.val = description
	i.description.fn = nil
	return i
}

// TitleFunc sets the title func of the input field.
//
// The TitleFunc will be re-evaluated when the binding of the TitleFunc changes.
// This is useful when you want to display dynamic content and update the title
// when another part of your form changes.
//
// See README#Dynamic for more usage information.
func (i *Input) TitleFunc(f func() string, bindings any) *Input {
	i.title.fn = f
	i.title.bindings = bindings
	return i
}

// DescriptionFunc sets the description func of the input field.
//
// The DescriptionFunc will be re-evaluated when the binding of the
// DescriptionFunc changes. This is useful when you want to display dynamic
// content and update the description when another part of your form changes.
//
// See README#Dynamic for more usage information.
func (i *Input) DescriptionFunc(f func() string, bindings any) *Input {
	i.description.fn = f
	i.description.bindings = bindings
	return i
}

// Prompt sets the prompt of the input field.
func (i *Input) Prompt(prompt string) *Input {
	i.textinput.Prompt = prompt
	return i
}

// CharLimit sets the character limit of the input field.
func (i *Input) CharLimit(charlimit int) *Input {
	i.textinput.CharLimit = charlimit
	return i
}

// Suggestions sets the suggestions to display for autocomplete in the input
// field.
//
// The suggestions are static for dynamic suggestions use `SuggestionsFunc`.
func (i *Input) Suggestions(suggestions []string) *Input {
	i.suggestions.fn = nil

	i.textinput.ShowSuggestions = len(suggestions) > 0
	i.textinput.KeyMap.AcceptSuggestion.SetEnabled(len(suggestions) > 0)
	i.textinput.SetSuggestions(suggestions)
	return i
}

// SuggestionsFunc sets the suggestions func to display for autocomplete in the
// input field.
//
// The SuggestionsFunc will be re-evaluated when the binding of the
// SuggestionsFunc changes. This is useful when you want to display dynamic
// suggestions when another part of your form changes.
//
// See README#Dynamic for more usage information.
func (i *Input) SuggestionsFunc(f func() []string, bindings any) *Input {
	i.suggestions.fn = f
	i.suggestions.bindings = bindings
	i.suggestions.loading = true

	i.textinput.KeyMap.AcceptSuggestion.SetEnabled(f != nil)
	i.textinput.ShowSuggestions = f != nil
	return i
}

// EchoMode sets the input behavior of the text Input field.
type EchoMode textinput.EchoMode

const (
	// EchoModeNormal displays text as is.
	// This is the default behavior.
	EchoModeNormal EchoMode = EchoMode(textinput.EchoNormal)

	// EchoModePassword displays the EchoCharacter mask instead of actual characters.
	// This is commonly used for password fields.
	EchoModePassword EchoMode = EchoMode(textinput.EchoPassword)

	// EchoModeNone displays nothing as characters are entered.
	// This is commonly seen for password fields on the command line.
	EchoModeNone EchoMode = EchoMode(textinput.EchoNone)
)

// EchoMode sets the echo mode of the input.
func (i *Input) EchoMode(mode EchoMode) *Input {
	i.textinput.EchoMode = textinput.EchoMode(mode)
	return i
}

// Password sets whether or not to hide the input while the user is typing.
//
// Deprecated: use EchoMode(EchoPassword) instead.
func (i *Input) Password(password bool) *Input {
	if password {
		i.textinput.EchoMode = textinput.EchoPassword
	} else {
		i.textinput.EchoMode = textinput.EchoNormal
	}
	return i
}

// Placeholder sets the placeholder of the text input.
func (i *Input) Placeholder(str string) *Input {
	i.textinput.Placeholder = str
	return i
}

// PlaceholderFunc sets the placeholder func of the text input.
func (i *Input) PlaceholderFunc(f func() string, bindings any) *Input {
	i.placeholder.fn = f
	i.placeholder.bindings = bindings
	return i
}

// Inline sets whether the title and input should be on the same line.
func (i *Input) Inline(inline bool) *Input {
	i.inline = inline
	return i
}

// Validate sets the validation function of the input field.
func (i *Input) Validate(validate func(string) error) *Input {
	i.validate = validate
	return i
}

// Error returns the error of the input field.
func (i *Input) Error() error { return i.err }

// Skip returns whether the input should be skipped or should be blocking.
func (*Input) Skip() bool { return false }

// Zoom returns whether the input should be zoomed.
func (*Input) Zoom() bool { return false }

// Focus focuses the input field.
func (i *Input) Focus() tea.Cmd {
	i.focused = true
	return i.textinput.Focus()
}

// Blur blurs the input field.
func (i *Input) Blur() tea.Cmd {
	i.focused = false
	i.accessor.Set(i.textinput.Value())
	i.textinput.Blur()
	i.err = i.validate(i.accessor.Get())
	return nil
}

// KeyBinds returns the help message for the input field.
func (i *Input) KeyBinds() []key.Binding {
	if i.textinput.ShowSuggestions {
		return []key.Binding{i.keymap.AcceptSuggestion, i.keymap.Prev, i.keymap.Submit, i.keymap.Next}
	}
	return []key.Binding{i.keymap.Prev, i.keymap.Submit, i.keymap.Next}
}

// Init initializes the input field.
func (i *Input) Init() tea.Cmd {
	i.textinput.Blur()
	return nil
}

// Update updates the input field.
func (i *Input) Update(msg tea.Msg) (Model, tea.Cmd) {
	var cmds []tea.Cmd //nolint:prealloc

	switch msg := msg.(type) {
	case tea.BackgroundColorMsg:
		i.hasDarkBg = msg.IsDark()
	case updateFieldMsg:
		var cmds []tea.Cmd
		if ok, hash := i.title.shouldUpdate(); ok {
			i.title.bindingsHash = hash
			if !i.title.loadFromCache() {
				i.title.loading = true
				cmds = append(cmds, func() tea.Msg {
					return updateTitleMsg{id: i.id, title: i.title.fn(), hash: hash}
				})
			}
		}
		if ok, hash := i.description.shouldUpdate(); ok {
			i.description.bindingsHash = hash
			if !i.description.loadFromCache() {
				i.description.loading = true
				cmds = append(cmds, func() tea.Msg {
					return updateDescriptionMsg{id: i.id, description: i.description.fn(), hash: hash}
				})
			}
		}
		if ok, hash := i.placeholder.shouldUpdate(); ok {
			i.placeholder.bindingsHash = hash
			if i.placeholder.loadFromCache() {
				i.textinput.Placeholder = i.placeholder.val
			} else {
				i.placeholder.loading = true
				cmds = append(cmds, func() tea.Msg {
					return updatePlaceholderMsg{id: i.id, placeholder: i.placeholder.fn(), hash: hash}
				})
			}
		}
		if ok, hash := i.suggestions.shouldUpdate(); ok {
			i.suggestions.bindingsHash = hash
			if i.suggestions.loadFromCache() {
				i.textinput.ShowSuggestions = len(i.suggestions.val) > 0
				i.textinput.SetSuggestions(i.suggestions.val)
			} else {
				i.suggestions.loading = true
				cmds = append(cmds, func() tea.Msg {
					return updateSuggestionsMsg{id: i.id, suggestions: i.suggestions.fn(), hash: hash}
				})
			}
		}
		return i, tea.Batch(cmds...)
	case updateTitleMsg:
		if i.id == msg.id && i.title.bindingsHash == msg.hash {
			i.title.update(msg.title)
		}
	case updateDescriptionMsg:
		if i.id == msg.id && i.description.bindingsHash == msg.hash {
			i.description.update(msg.description)
		}
	case updatePlaceholderMsg:
		if i.id == msg.id && i.placeholder.bindingsHash == msg.hash {
			i.placeholder.update(msg.placeholder)
			i.textinput.Placeholder = msg.placeholder
		}
	case updateSuggestionsMsg:
		if i.id == msg.id && i.suggestions.bindingsHash == msg.hash {
			i.suggestions.update(msg.suggestions)
			i.textinput.ShowSuggestions = len(msg.suggestions) > 0
			i.textinput.SetSuggestions(msg.suggestions)
		}
	case tea.KeyPressMsg:
		i.err = nil

		switch {
		case key.Matches(msg, i.keymap.Prev):
			cmds = append(cmds, PrevField)
		case key.Matches(msg, i.keymap.Next, i.keymap.Submit):
			value := i.textinput.Value()
			i.err = i.validate(value)
			if i.err != nil {
				return i, nil
			}
			cmds = append(cmds, NextField)
		}
	}

	var cmd tea.Cmd
	i.textinput, cmd = i.textinput.Update(msg)
	cmds = append(cmds, cmd)
	i.accessor.Set(i.textinput.Value())

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

func (i *Input) activeStyles() *FieldStyles {
	theme := i.theme
	if theme == nil {
		theme = ThemeFunc(ThemeCharm)
	}
	if i.focused {
		return &theme.Theme(i.hasDarkBg).Focused
	}
	return &theme.Theme(i.hasDarkBg).Blurred
}

// View renders the input field.
func (i *Input) View() string {
	styles := i.activeStyles()
	maxWidth := i.width - styles.Base.GetHorizontalFrameSize()

	// NB: since the method is on a pointer receiver these are being mutated.
	// Because this runs on every render this shouldn't matter in practice,
	// however.
	st := i.textinput.Styles()
	st.Cursor.Color = styles.TextInput.Cursor.GetForeground()
	st.Focused.Prompt = styles.TextInput.Prompt
	st.Focused.Text = styles.TextInput.Text
	st.Focused.Placeholder = styles.TextInput.Placeholder
	i.textinput.SetStyles(st)

	// Adjust text input size to its char limit if it fit in its width
	if i.textinput.CharLimit > 0 {
		i.textinput.SetWidth(max(min(i.textinput.CharLimit, i.textinput.Width(), maxWidth), 0))
	}

	var sb strings.Builder
	if i.title.val != "" || i.title.fn != nil {
		sb.WriteString(styles.Title.Render(wrap(i.title.val, maxWidth)))
		if !i.inline {
			sb.WriteString("\n")
		}
	}
	if i.description.val != "" || i.description.fn != nil {
		sb.WriteString(styles.Description.Render(wrap(i.description.val, maxWidth)))
		if !i.inline {
			sb.WriteString("\n")
		}
	}
	sb.WriteString(i.textinput.View())

	return styles.Base.
		Width(i.width).
		Height(i.height).
		Render(sb.String())
}

// Run runs the input field in accessible mode.
func (i *Input) Run() error {
	return i.run()
}

// run runs the input field.
func (i *Input) run() error {
	return Run(i)
}

// RunAccessible runs the input field in accessible mode.
func (i *Input) RunAccessible(w io.Writer, r io.Reader) error {
	styles := i.activeStyles()
	validator := func(input string) error {
		if i.textinput.CharLimit > 0 && len(input) > i.textinput.CharLimit {
			return fmt.Errorf("Input cannot exceed %d characters", i.textinput.CharLimit)
		}
		return i.validate(input)
	}

	switch i.textinput.EchoMode {
	case textinput.EchoNormal:
		prompt := styles.Title.
			PaddingRight(1).
			Render(cmp.Or(i.title.val, "Input:"))
		value := accessibility.PromptString(w, r, prompt, i.GetValue().(string), validator)
		i.accessor.Set(value)
		return nil
	default:
		prompt := styles.Title.
			PaddingRight(1).
			Render(cmp.Or(i.title.val, "Password:"))
		if fd, ok := r.(interface{ Fd() uintptr }); ok {
			value, err := accessibility.PromptPassword(w, fd.Fd(), prompt, validator)
			if err != nil {
				return err //nolint:wrapcheck
			}
			i.accessor.Set(value)
			return nil
		}
		return errors.New("password asking needs a tty")
	}
}

// WithKeyMap sets the keymap on an input field.
func (i *Input) WithKeyMap(k *KeyMap) Field {
	i.keymap = k.Input
	i.textinput.KeyMap.AcceptSuggestion = i.keymap.AcceptSuggestion
	return i
}

// WithTheme sets the theme of the input field.
func (i *Input) WithTheme(theme Theme) Field {
	if i.theme != nil {
		return i
	}
	i.theme = theme
	return i
}

// WithWidth sets the width of the input field.
func (i *Input) WithWidth(width int) Field {
	styles := i.activeStyles()
	i.width = width
	frameSize := styles.Base.GetHorizontalFrameSize()
	promptWidth := lipgloss.Width(i.textinput.Styles().Focused.Prompt.Render(i.textinput.Prompt))
	titleWidth := lipgloss.Width(styles.Title.Render(i.title.val))
	descriptionWidth := lipgloss.Width(styles.Description.Render(i.description.val))
	i.textinput.SetWidth(width - frameSize - promptWidth - 1)
	if i.inline {
		i.textinput.SetWidth(i.textinput.Width() - titleWidth - descriptionWidth)
	}
	return i
}

// WithHeight sets the height of the input field.
func (i *Input) WithHeight(height int) Field {
	i.height = height
	return i
}

// WithPosition sets the position of the input field.
func (i *Input) WithPosition(p FieldPosition) Field {
	i.keymap.Prev.SetEnabled(!p.IsFirst())
	i.keymap.Next.SetEnabled(!p.IsLast())
	i.keymap.Submit.SetEnabled(p.IsLast())
	return i
}

// GetKey returns the key of the field.
func (i *Input) GetKey() string { return i.key }

// GetValue returns the value of the field.
func (i *Input) GetValue() any {
	return i.accessor.Get()
}


================================================
FILE: field_multiselect.go
================================================
package huh

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

	"charm.land/bubbles/v2/key"
	"charm.land/bubbles/v2/spinner"
	"charm.land/bubbles/v2/textinput"
	"charm.land/bubbles/v2/viewport"
	tea "charm.land/bubbletea/v2"
	"charm.land/huh/v2/internal/accessibility"
	"charm.land/lipgloss/v2"
	"github.com/charmbracelet/x/exp/ordered"
)

// MultiSelect is a form multi-select field.
type MultiSelect[T comparable] struct {
	accessor Accessor[[]T]
	key      string
	id       int

	// customization
	title           Eval[string]
	description     Eval[string]
	options         Eval[[]Option[T]]
	filterable      bool
	filteredOptions []Option[T]
	limit           int

	// error handling
	validate func([]T) error
	err      error

	// state
	cursor    int
	focused   bool
	filtering bool
	filter    textinput.Model
	viewport  viewport.Model
	spinner   spinner.Model

	// options
	width     int
	height    int
	theme     Theme
	hasDarkBg bool
	keymap    MultiSelectKeyMap
}

// NewMultiSelect returns a new multi-select field.
func NewMultiSelect[T comparable]() *MultiSelect[T] {
	filter := textinput.New()
	filter.Prompt = "/"

	s := spinner.New(spinner.WithSpinner(spinner.Line))

	return &MultiSelect[T]{
		accessor:    &EmbeddedAccessor[[]T]{},
		validate:    func([]T) error { return nil },
		filtering:   false,
		filter:      filter,
		id:          nextID(),
		options:     Eval[[]Option[T]]{cache: make(map[uint64][]Option[T])},
		title:       Eval[string]{cache: make(map[uint64]string)},
		description: Eval[string]{cache: make(map[uint64]string)},
		spinner:     s,
		filterable:  true,
	}
}

// Value sets the value of the multi-select field.
func (m *MultiSelect[T]) Value(value *[]T) *MultiSelect[T] {
	return m.Accessor(NewPointerAccessor(value))
}

// Accessor sets the accessor of the input field.
func (m *MultiSelect[T]) Accessor(accessor Accessor[[]T]) *MultiSelect[T] {
	m.accessor = accessor
	for i, o := range m.options.val {
		if slices.Contains(m.accessor.Get(), o.Value) {
			m.options.val[i].selected = true
		}
	}
	return m
}

// Key sets the key of the select field which can be used to retrieve the value
// after submission.
func (m *MultiSelect[T]) Key(key string) *MultiSelect[T] {
	m.key = key
	return m
}

// Title sets the title of the multi-select field.
func (m *MultiSelect[T]) Title(title string) *MultiSelect[T] {
	m.title.val = title
	m.title.fn = nil
	return m
}

// TitleFunc sets the title func of the multi-select field.
func (m *MultiSelect[T]) TitleFunc(f func() string, bindings any) *MultiSelect[T] {
	m.title.fn = f
	m.title.bindings = bindings
	return m
}

// Description sets the description of the multi-select field.
func (m *MultiSelect[T]) Description(description string) *MultiSelect[T] {
	m.description.val = description
	return m
}

// DescriptionFunc sets the description func of the multi-select field.
func (m *MultiSelect[T]) DescriptionFunc(f func() string, bindings any) *MultiSelect[T] {
	m.description.fn = f
	m.description.bindings = bindings
	return m
}

// Options sets the options of the multi-select field.
func (m *MultiSelect[T]) Options(options ...Option[T]) *MultiSelect[T] {
	if len(options) <= 0 {
		return m
	}

	m.options.val = options
	m.filteredOptions = options
	m.selectOptions()
	m.updateViewportSize()
	return m
}

func (m *MultiSelect[T]) selectOptions() {
	// Set the cursor to the existing value or the last selected option.
	for i, o := range m.options.val {
		for _, v := range m.accessor.Get() {
			if o.Value == v {
				m.options.val[i].selected = true
			}
		}
	}

	for i, o := range m.options.val {
		if !o.selected {
			continue
		}
		m.cursor = i
		m.ensureCursorVisible()
		break
	}
}

// OptionsFunc sets the options func of the multi-select field.
func (m *MultiSelect[T]) OptionsFunc(f func() []Option[T], bindings any) *MultiSelect[T] {
	m.options.fn = f
	m.options.bindings = bindings
	m.filteredOptions = make([]Option[T], 0)
	// If there is no height set, we should attach a static height since these
	// options are possibly dynamic.
	if m.height <= 0 {
		m.height = defaultHeight
		m.updateViewportSize()
	}
	if m.width <= 0 {
		m.Width(20)
	}
	return m
}

// Filterable sets the multi-select field as filterable.
func (m *MultiSelect[T]) Filterable(filterable bool) *MultiSelect[T] {
	m.filterable = filterable
	return m
}

// Filtering sets the filtering state of the multi-select field.
func (m *MultiSelect[T]) Filtering(filtering bool) *MultiSelect[T] {
	m.filtering = filtering
	m.filter.Focus()
	return m
}

// Limit sets the limit of the multi-select field.
func (m *MultiSelect[T]) Limit(limit int) *MultiSelect[T] {
	m.limit = limit
	m.setSelectAllHelp()
	return m
}

// Width sets the width of the multi-select field.
func (m *MultiSelect[T]) Width(width int) *MultiSelect[T] {
	// What we really want to do is set the width of the viewport, but we
	// need a theme applied before we can calcualate its width.
	m.width = width
	m.updateViewportSize()
	return m
}

// Height sets the height of the multi-select field.
func (m *MultiSelect[T]) Height(height int) *MultiSelect[T] {
	// What we really want to do is set the height of the viewport, but we
	// need a theme applied before we can calcualate its height.
	m.height = height
	m.updateViewportSize()
	return m
}

// Validate sets the validation function of the multi-select field.
func (m *MultiSelect[T]) Validate(validate func([]T) error) *MultiSelect[T] {
	m.validate = validate
	return m
}

// Error returns the error of the multi-select field.
func (m *MultiSelect[T]) Error() error {
	return m.err
}

// Skip returns whether the multiselect should be skipped or should be blocking.
func (*MultiSelect[T]) Skip() bool {
	return false
}

// Zoom returns whether the multiselect should be zoomed.
func (*MultiSelect[T]) Zoom() bool {
	return false
}

// Focus focuses the multi-select field.
func (m *MultiSelect[T]) Focus() tea.Cmd {
	m.updateValue()
	m.focused = true
	return nil
}

// Blur blurs the multi-select field.
func (m *MultiSelect[T]) Blur() tea.Cmd {
	m.updateValue()
	m.focused = false
	return nil
}

// Hovered returns the value of the option under the cursor, and a bool
// indicating whether one was found. If there are no visible options, returns
// a zero-valued T and false.
func (m *MultiSelect[T]) Hovered() (T, bool) {
	if len(m.filteredOptions) == 0 || m.cursor >= len(m.filteredOptions) {
		var zero T
		return zero, false
	}
	return m.filteredOptions[m.cursor].Value, true
}

// KeyBinds returns the help message for the multi-select field.
func (m *MultiSelect[T]) KeyBinds() []key.Binding {
	m.setSelectAllHelp()
	binds := []key.Binding{
		m.keymap.Toggle,
		m.keymap.Up,
		m.keymap.Down,
	}
	if m.filterable {
		binds = append(
			binds,
			m.keymap.Filter,
			m.keymap.SetFilter,
			m.keymap.ClearFilter,
		)
	}
	binds = append(
		binds,
		m.keymap.Prev,
		m.keymap.Submit,
		m.keymap.Next,
		m.keymap.SelectAll,
		m.keymap.SelectNone,
	)
	return binds
}

// Init initializes the multi-select field.
func (m *MultiSelect[T]) Init() tea.Cmd {
	return nil
}

// Update updates the multi-select field.
func (m *MultiSelect[T]) Update(msg tea.Msg) (Model, tea.Cmd) {
	var cmds []tea.Cmd

	// Enforce height on the viewport during update as we need themes to
	// be applied before we can calculate the height.
	m.updateViewportSize()

	var cmd tea.Cmd
	if m.filtering {
		m.filter, cmd = m.filter.Update(msg)
		m.setSelectAllHelp()
		cmds = append(cmds, cmd)
	}

	switch msg := msg.(type) {
	case tea.BackgroundColorMsg:
		m.hasDarkBg = msg.IsDark()
	case updateFieldMsg:
		var fieldCmds []tea.Cmd
		if ok, hash := m.title.shouldUpdate(); ok {
			m.title.bindingsHash = hash
			if !m.title.loadFromCache() {
				m.title.loading = true
				fieldCmds = append(fieldCmds, func() tea.Msg {
					return updateTitleMsg{id: m.id, title: m.title.fn(), hash: hash}
				})
			}
		}
		if ok, hash := m.description.shouldUpdate(); ok {
			m.description.bindingsHash = hash
			if !m.description.loadFromCache() {
				m.description.loading = true
				fieldCmds = append(fieldCmds, func() tea.Msg {
					return updateDescriptionMsg{id: m.id, description: m.description.fn(), hash: hash}
				})
			}
		}
		if ok, hash := m.options.shouldUpdate(); ok {
			m.options.bindingsHash = hash
			if m.options.loadFromCache() {
				m.filteredOptions = m.options.val
				m.updateValue()
				m.cursor = ordered.Clamp(m.cursor, 0, len(m.filteredOptions)-1)
			} else {
				m.options.loading = true
				m.options.loadingStart = time.Now()
				fieldCmds = append(fieldCmds, func() tea.Msg {
					return updateOptionsMsg[T]{id: m.id, options: m.options.fn(), hash: hash}
				}, m.spinner.Tick)
			}
		}

		return m, tea.Batch(fieldCmds...)

	case spinner.TickMsg:
		if !m.options.loading {
			break
		}
		m.spinner, cmd = m.spinner.Update(msg)
		return m, cmd

	case updateTitleMsg:
		if msg.id == m.id && msg.hash == m.title.bindingsHash {
			m.title.update(msg.title)
		}
	case updateDescriptionMsg:
		if msg.id == m.id && msg.hash == m.description.bindingsHash {
			m.description.update(msg.description)
		}
	case updateOptionsMsg[T]:
		if msg.id == m.id && msg.hash == m.options.bindingsHash {
			m.options.update(msg.options)
			m.selectOptions()
			// since we're updating the options, we need to reset the cursor.
			m.filteredOptions = m.options.val
			m.updateValue()
			m.cursor = ordered.Clamp(m.cursor, 0, len(m.filteredOptions)-1)
		}
	case tea.KeyPressMsg:
		m.err = nil
		switch {
		case key.Matches(msg, m.keymap.Filter):
			m.setFilter(true)
			return m, m.filter.Focus()
		case key.Matches(msg, m.keymap.SetFilter):
			if len(m.filteredOptions) <= 0 {
				m.filter.SetValue("")
				m.filteredOptions = m.options.val
			}
			m.setFilter(false)
		case key.Matches(msg, m.keymap.ClearFilter):
			m.filter.SetValue("")
			m.filteredOptions = m.options.val
			m.setFilter(false)
		case key.Matches(msg, m.keymap.Up):
			//nolint:godox
			// FIXME: should use keys in keymap
			if m.filtering && msg.String() == "k" {
				break
			}

			m.cursor = max(m.cursor-1, 0)
			m.ensureCursorVisible()
		case key.Matches(msg, m.keymap.Down):
			//nolint:godox
			// FIXME: should use keys in keymap
			if m.filtering && msg.String() == "j" {
				break
			}

			m.cursor = min(m.cursor+1, len(m.filteredOptions)-1)
			m.ensureCursorVisible()
		case key.Matches(msg, m.keymap.GotoTop):
			if m.filtering {
				break
			}
			m.cursor = 0
			m.viewport.GotoTop()
		case key.Matches(msg, m.keymap.GotoBottom):
			if m.filtering {
				break
			}
			m.cursor = len(m.filteredOptions) - 1
			m.viewport.GotoBottom()
		case key.Matches(msg, m.keymap.HalfPageUp):
			m.cursor = max(m.cursor-m.viewport.Height()/2, 0)
			m.ensureCursorVisible()
		case key.Matches(msg, m.keymap.HalfPageDown):
			m.cursor = min(m.cursor+m.viewport.Height()/2, len(m.filteredOptions)-1)
			m.ensureCursorVisible()
		case key.Matches(msg, m.keymap.Toggle) && !m.filtering:
			for i, option := range m.options.val {
				if option.Key == m.filteredOptions[m.cursor].Key {
					if !m.options.val[m.cursor].selected && m.limit > 0 && m.numSelected() >= m.limit {
						break
					}
					selected := m.options.val[i].selected
					m.options.val[i].selected = !selected
					m.filteredOptions[m.cursor].selected = !selected
				}
			}
			m.setSelectAllHelp()
			m.updateValue()
		case key.Matches(msg, m.keymap.SelectAll, m.keymap.SelectNone) && m.limit <= 0:
			selected := false

			for _, option := range m.filteredOptions {
				if !option.selected {
					selected = true
					break
				}
			}

			for i, option := range m.options.val {
				for j := range m.filteredOptions {
					if option.Key == m.filteredOptions[j].Key {
						m.options.val[i].selected = selected
						m.filteredOptions[j].selected = selected
						break
					}
				}
			}
			m.setSelectAllHelp()
			m.updateValue()
		case key.Matches(msg, m.keymap.Prev):
			m.updateValue()
			m.err = m.validate(m.accessor.Get())
			if m.err != nil {
				return m, nil
			}
			return m, PrevField
		case key.Matches(msg, m.keymap.Next, m.keymap.Submit):
			m.updateValue()
			m.err = m.validate(m.accessor.Get())
			if m.err != nil {
				return m, nil
			}
			return m, NextField
		}

		if m.filtering {
			m.filteredOptions = m.options.val
			if m.filter.Value() != "" {
				m.filteredOptions = nil
				for _, option := range m.options.val {
					if m.filterFunc(option.Key) {
						m.filteredOptions = append(m.filteredOptions, option)
					}
				}
			}
			if len(m.filteredOptions) > 0 {
				m.cursor = min(m.cursor, len(m.filteredOptions)-1)
			}
		}
		m.ensureCursorVisible()
	}

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

// updateViewportSize updates the viewport size according to the Height setting
// on this multi-select field.
func (m *MultiSelect[T]) updateViewportSize() {
	yoffset := 0
	if ss := m.titleView(); ss != "" {
		yoffset += lipgloss.Height(ss)
	}
	if ss := m.descriptionView(); ss != "" {
		yoffset += lipgloss.Height(ss)
	}
	v, _, _ := m.optionsView()
	height := m.height
	if height <= 0 {
		height = lipgloss.Height(v)
	}
	width := m.width
	if m.width <= 0 {
		width = lipgloss.Width(v)
	}

	m.viewport.SetWidth(width)
	m.viewport.SetHeight(max(minHeight, height) - yoffset)
}

// numSelected returns the total number of selected options.
func (m *MultiSelect[T]) numSelected() int {
	var count int
	for _, o := range m.options.val {
		if o.selected {
			count++
		}
	}
	return count
}

// numFilteredOptionsSelected returns the number of selected options with the
// current filter applied.
func (m *MultiSelect[T]) numFilteredSelected() int {
	var count int
	for _, o := range m.filteredOptions {
		if o.selected {
			count++
		}
	}
	return count
}

func (m *MultiSelect[T]) updateValue() {
	value := make([]T, 0)
	for _, option := range m.options.val {
		if option.selected {
			value = append(value, option.Value)
		}
	}
	m.accessor.Set(value)
	m.err = m.validate(m.accessor.Get())
}

func (m *MultiSelect[T]) activeStyles() *FieldStyles {
	theme := m.theme
	if theme == nil {
		theme = ThemeFunc(ThemeCharm)
	}
	if m.focused {
		return &theme.Theme(m.hasDarkBg).Focused
	}
	return &theme.Theme(m.hasDarkBg).Blurred
}

func (m *MultiSelect[T]) titleView() string {
	if m.title.val == "" {
		return ""
	}
	var (
		styles   = m.activeStyles()
		sb       = strings.Builder{}
		maxWidth = m.width - styles.Base.GetHorizontalFrameSize()
	)
	if m.filtering {
		sb.WriteString(m.filter.View())
	} else if m.filter.Value() != "" {
		sb.WriteString(styles.Title.Render(wrap(m.title.val, maxWidth)))
		sb.WriteString(styles.Description.Render("/" + m.filter.Value()))
	} else {
		sb.WriteString(styles.Title.Render(wrap(m.title.val, maxWidth)))
	}
	if m.err != nil {
		sb.WriteString(styles.ErrorIndicator.String())
	}
	return sb.String()
}

func (m *MultiSelect[T]) descriptionView() string {
	if m.description.val == "" {
		return ""
	}
	maxWidth := m.width - m.activeStyles().Base.GetHorizontalFrameSize()
	return m.activeStyles().Description.Render(wrap(m.description.val, maxWidth))
}

func (m *MultiSelect[T]) renderOption(option Option[T], cursor, selected bool) string {
	styles := m.activeStyles()
	var parts []string
	if cursor {
		parts = append(parts, styles.MultiSelectSelector.String())
	} else {
		parts = append(parts, strings.Repeat(" ", lipgloss.Width(styles.MultiSelectSelector.String())))
	}
	if selected {
		parts = append(parts, styles.SelectedPrefix.String())
		parts = append(parts, styles.SelectedOption.Render(option.Key))
	} else {
		parts = append(parts, styles.UnselectedPrefix.String())
		parts = append(parts, styles.UnselectedOption.Render(option.Key))
	}
	return lipgloss.JoinHorizontal(lipgloss.Left, parts...)
}

// cursorLineOffset computes the line offset and height (in lines) for the
// current cursor position without rendering the full options string.
func (m *MultiSelect[T]) cursorLineOffset() (offset int, height int) {
	for i, option := range m.filteredOptions {
		line := m.renderOption(option, m.cursor == i, m.filteredOptions[i].selected)
		h := lipgloss.Height(line)
		if i < m.cursor {
			offset += h
		}
		if i == m.cursor {
			height = h
			return offset, height
		}
	}
	return offset, height
}

func (m *MultiSelect[T]) ensureCursorVisible() {
	offset, height := m.cursorLineOffset()
	ensureVisible(&m.viewport, offset, height)
}

func (m *MultiSelect[T]) optionsView() (string, int, int) {
	var sb strings.Builder

	if m.options.loading && time.Since(m.options.loadingStart) > spinnerShowThreshold {
		m.spinner.Style = m.activeStyles().MultiSelectSelector.UnsetString()
		sb.WriteString(m.spinner.View() + " Loading...")
		return sb.String(), -1, 1
	}

	var cursorOffset int
	var cursorHeight int
	for i, option := range m.filteredOptions {
		cursor := m.cursor == i
		line := m.renderOption(option, cursor, m.filteredOptions[i].selected)
		if i < m.cursor {
			cursorOffset += lipgloss.Height(line)
		}
		if cursor {
			cursorHeight = lipgloss.Height(line)
		}
		sb.WriteString(line)
		if i < len(m.options.val)-1 {
			sb.WriteString("\n")
		}
	}

	for i := len(m.filteredOptions); i < len(m.options.val)-1; i++ {
		sb.WriteString("\n")
	}

	return sb.String(), cursorOffset, cursorHeight
}

// View renders the multi-select field.
func (m *MultiSelect[T]) View() string {
	styles := m.activeStyles()

	vpc, _, _ := m.optionsView()
	m.viewport.SetContent(vpc)

	var sb strings.Builder
	if m.title.val != "" || m.title.fn != nil {
		sb.WriteString(m.titleView())
		sb.WriteString("\n")
	}
	if m.description.val != "" || m.description.fn != nil {
		sb.WriteString(m.descriptionView() + "\n")
	}
	sb.WriteString(m.viewport.View())
	return styles.Base.Width(m.width).Height(m.height).
		Render(sb.String())
}

func (m *MultiSelect[T]) printOptions(w io.Writer) {
	styles := m.activeStyles()
	var sb strings.Builder
	for i, option := range m.options.val {
		if option.selected {
			sb.WriteString(styles.SelectedOption.Render(fmt.Sprintf("%d. %s %s", i+1, "✓", option.Key)))
		} else {
			_, _ = fmt.Fprintf(&sb, "%d.   %s", i+1, option.Key)
		}
		sb.WriteString("\n")
	}
	sb.WriteString("0.   Confirm selection\n")
	_, _ = fmt.Fprint(w, sb.String())
}

// setFilter sets the filter of the select field.
func (m *MultiSelect[T]) setFilter(filter bool) {
	m.filtering = filter
	m.keymap.SetFilter.SetEnabled(filter)
	m.keymap.Filter.SetEnabled(!filter)
	m.keymap.Next.SetEnabled(!filter)
	m.keymap.Submit.SetEnabled(!filter)
	m.keymap.Prev.SetEnabled(!filter)
	m.keymap.ClearFilter.SetEnabled(!filter && m.filter.Value() != "")
}

// filterFunc returns true if the option matches the filter.
func (m *MultiSelect[T]) filterFunc(option string) bool {
	// XXX: remove diacritics or allow customization of filter function.
	return strings.Contains(strings.ToLower(option), strings.ToLower(m.filter.Value()))
}

// setSelectAllHelp enables the appropriate select all or select none keybinding.
func (m *MultiSelect[T]) setSelectAllHelp() {
	if m.limit > 0 {
		m.keymap.SelectAll.SetEnabled(false)
		m.keymap.SelectNone.SetEnabled(false)
		return
	}

	noneSelected := m.numFilteredSelected() <= 0
	allSelected := m.numFilteredSelected() > 0 && m.numFilteredSelected() < len(m.filteredOptions)
	selectAll := noneSelected || allSelected
	m.keymap.SelectAll.SetEnabled(selectAll)
	m.keymap.SelectNone.SetEnabled(!selectAll)
}

// Run runs the multi-select field.
func (m *MultiSelect[T]) Run() error {
	return Run(m)
}

// RunAccessible runs the multi-select field in accessible mode.
func (m *MultiSelect[T]) RunAccessible(w io.Writer, r io.Reader) error {
	styles := m.activeStyles()
	title := styles.Title.
		PaddingRight(1).
		Render(cmp.Or(m.title.val, "Select:"))
	_, _ = fmt.Fprintln(w, title)
	limit := m.limit
	if limit == 0 {
		limit = len(m.options.val)
	}
	_, _ = fmt.Fprintf(w, "Select up to %d options.\n", limit)

	var choice int
	for {
		m.printOptions(w)

		prompt := fmt.Sprintf("Enter a number between %d and %d: ", 0, len(m.options.val))
		choice = accessibility.PromptInt(w, r, prompt, 0, len(m.options.val), nil)
		if choice <= 0 {
			m.updateValue()
			err := m.validate(m.accessor.Get())
			if err != nil {
				_, _ = fmt.Fprintln(w, err)
				continue
			}
			break
		}

		if !m.options.val[choice-1].selected && m.limit > 0 && m.numSelected() >= m.limit {
			_, _ = fmt.Fprintf(w, "You can't select more than %d options.\n", m.limit)
			_, _ = fmt.Fprintln(w)
			continue
		}
		m.options.val[choice-1].selected = !m.options.val[choice-1].selected
		_, _ = fmt.Fprintln(w)
	}

	return nil
}

// WithTheme sets the theme of the multi-select field.
func (m *MultiSelect[T]) WithTheme(theme Theme) Field {
	if m.theme != nil {
		return m
	}
	m.theme = theme
	styles := m.theme.Theme(m.hasDarkBg)

	st := m.filter.Styles()
	st.Cursor.Color = styles.Focused.TextInput.Cursor.GetForeground()
	st.Focused.Prompt = styles.Focused.TextInput.Prompt
	st.Focused.Text = styles.Focused.TextInput.Text
	st.Focused.Placeholder = styles.Focused.TextInput.Placeholder
	m.filter.SetStyles(st)

	m.updateViewportSize()
	return m
}

// WithKeyMap sets the keymap of the multi-select field.
func (m *MultiSelect[T]) WithKeyMap(k *KeyMap) Field {
	m.keymap = k.MultiSelect
	if !m.filterable {
		m.keymap.Filter.SetEnabled(false)
		m.keymap.ClearFilter.SetEnabled(false)
		m.keymap.SetFilter.SetEnabled(false)
	}
	return m
}

// WithWidth sets the width of the multi-select field.
func (m *MultiSelect[T]) WithWidth(width int) Field {
	m.width = width
	m.updateViewportSize()
	return m
}

// WithHeight sets the total height of the multi-select field. Including padding
// and help menu heights.
func (m *MultiSelect[T]) WithHeight(height int) Field {
	m.Height(height)
	return m
}

// WithPosition sets the position of the multi-select field.
func (m *MultiSelect[T]) WithPosition(p FieldPosition) Field {
	if m.filtering {
		return m
	}
	m.keymap.Prev.SetEnabled(!p.IsFirst())
	m.keymap.Next.SetEnabled(!p.IsLast())
	m.keymap.Submit.SetEnabled(p.IsLast())
	return m
}

// GetKey returns the multi-select's key.
func (m *MultiSelect[T]) GetKey() string {
	return m.key
}

// GetValue returns the multi-select's value.
func (m *MultiSelect[T]) GetValue() any {
	return m.accessor.Get()
}

// GetFiltering returns whether the multi-select is filtering.
func (m *MultiSelect[T]) GetFiltering() bool {
	return m.filtering
}


================================================
FILE: field_note.go
================================================
package huh

import (
	"fmt"
	"io"
	"strings"

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

// Note is a note field.
//
// A note is responsible for displaying information to the user. Use it to
// provide context around a different field. Generally, the notes are not
// interacted with unless the note has a next button `Next(true)`.
type Note struct {
	id int

	title       Eval[string]
	description Eval[string]
	nextLabel   string

	focused        bool
	showNextButton bool
	skip           bool

	height int
	width  int

	theme     Theme
	hasDarkBg bool
	keymap    NoteKeyMap
}

// NewNote creates a new note field.
//
// A note is responsible for displaying information to the user. Use it to
// provide context around a different field. Generally, the notes are not
// interacted with unless the note has a next button `Next(true)`.
func NewNote() *Note {
	return &Note{
		id:             nextID(),
		showNextButton: false,
		skip:           true,
		nextLabel:      "Next",
		title:          Eval[string]{cache: make(map[uint64]string)},
		description:    Eval[string]{cache: make(map[uint64]string)},
	}
}

// Title sets the note field's title.
//
// This title will be static, for dynamic titles use `TitleFunc`.
func (n *Note) Title(title string) *Note {
	n.title.val = title
	n.title.fn = nil
	return n
}

// TitleFunc sets the title func of the note field.
//
// The TitleFunc will be re-evaluated when the binding of the TitleFunc changes.
// This is useful when you want to display dynamic content and update the title
// of a note when another part of your form changes.
//
// See README.md#Dynamic for more usage information.
func (n *Note) TitleFunc(f func() string, bindings any) *Note {
	n.title.fn = f
	n.title.bindings = bindings
	return n
}

// Description sets the note field's description.
//
// This description will be static, for dynamic descriptions use `DescriptionFunc`.
func (n *Note) Description(description string) *Note {
	n.description.val = description
	n.description.fn = nil
	return n
}

// DescriptionFunc sets the description func of the note field.
//
// The DescriptionFunc will be re-evaluated when the binding of the
// DescriptionFunc changes. This is useful when you want to display dynamic
// content and update the description of a note when another part of your form
// changes.
//
// For example, you can make a dynamic markdown preview with the following Form & Group.
//
//	huh.NewText().Title("Markdown").Value(&md),
//	huh.NewNote().Height(20).Title("Preview").
//	  DescriptionFunc(func() string {
//	      return md
//	  }, &md),
//
// Notice the `binding` of the Note is the same as the `Value` of the Text field.
// This binds the two values together, so that when the `Value` of the Text
// field changes so does the Note description.
func (n *Note) DescriptionFunc(f func() string, bindings any) *Note {
	n.description.fn = f
	n.description.bindings = bindings
	return n
}

// Height sets the note field's height.
func (n *Note) Height(height int) *Note {
	n.height = height
	return n
}

// Next sets whether or not to show the next button.
//
//	Title
//	Description
//
//	[ Next ]
func (n *Note) Next(show bool) *Note {
	n.showNextButton = show
	return n
}

// NextLabel sets the next button label.
func (n *Note) NextLabel(label string) *Note {
	n.nextLabel = label
	return n
}

// Focus focuses the note field.
func (n *Note) Focus() tea.Cmd {
	n.focused = true
	return nil
}

// Blur blurs the note field.
func (n *Note) Blur() tea.Cmd {
	n.focused = false
	return nil
}

// Error returns the error of the note field.
func (n *Note) Error() error { return nil }

// Skip returns whether the note should be skipped or should be blocking.
func (n *Note) Skip() bool { return n.skip }

// Zoom returns whether the note should be zoomed.
func (n *Note) Zoom() bool { return false }

// KeyBinds returns the help message for the note field.
func (n *Note) KeyBinds() []key.Binding {
	return []key.Binding{
		n.keymap.Prev,
		n.keymap.Submit,
		n.keymap.Next,
	}
}

// Init initializes the note field.
func (n *Note) Init() tea.Cmd { return nil }

// Update updates the note field.
func (n *Note) Update(msg tea.Msg) (Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.BackgroundColorMsg:
		n.hasDarkBg = msg.IsDark()
	case updateFieldMsg:
		var cmds []tea.Cmd
		if ok, hash := n.title.shouldUpdate(); ok {
			n.title.bindingsHash = hash
			if !n.title.loadFromCache() {
				n.title.loading = true
				cmds = append(cmds, func() tea.Msg {
					return updateTitleMsg{id: n.id, title: n.title.fn(), hash: hash}
				})
			}
		}
		if ok, hash := n.description.shouldUpdate(); ok {
			n.description.bindingsHash = hash
			if !n.description.loadFromCache() {
				n.description.loading = true
				cmds = append(cmds, func() tea.Msg {
					return updateDescriptionMsg{id: n.id, description: n.description.fn(), hash: hash}
				})
			}
		}
		return n, tea.Batch(cmds...)
	case updateTitleMsg:
		if msg.id == n.id && msg.hash == n.title.bindingsHash {
			n.title.update(msg.title)
		}
	case updateDescriptionMsg:
		if msg.id == n.id && msg.hash == n.description.bindingsHash {
			n.description.update(msg.description)
		}
	case tea.KeyPressMsg:
		switch {
		case key.Matches(msg, n.keymap.Prev):
			return n, PrevField
		case key.Matches(msg, n.keymap.Next, n.keymap.Submit):
			return n, NextField
		}
		return n, NextField
	}
	return n, nil
}

func (n *Note) activeStyles() *FieldStyles {
	theme := n.theme
	if theme == nil {
		theme = ThemeFunc(ThemeCharm)
	}
	if n.focused {
		return &theme.Theme(n.hasDarkBg).Focused
	}
	return &theme.Theme(n.hasDarkBg).Blurred
}

// View renders the note field.
func (n *Note) View() string {
	styles := n.activeStyles()
	maxWidth := n.width - styles.Card.GetHorizontalFrameSize()
	sb := strings.Builder{}

	if n.title.val != "" || n.title.fn != nil {
		sb.WriteString(styles.NoteTitle.Render(wrap(n.title.val, maxWidth)))
	}
	if n.description.val != "" || n.description.fn != nil {
		sb.WriteRune('\n')
		sb.WriteString(wrap(render(n.description.val), maxWidth))
		sb.WriteRune('\n')
	}
	if n.showNextButton {
		sb.WriteRune('\n')
		sb.WriteString(styles.Next.Render(n.nextLabel))
	}
	return styles.Card.
		Height(n.height).
		Width(n.width).
		Render(sb.String())
}

// Run runs the note field.
func (n *Note) Run() error {
	return Run(n)
}

// RunAccessible runs an accessible note field.
func (n *Note) RunAccessible(w io.Writer, _ io.Reader) error {
	styles := n.activeStyles()
	if n.title.val != "" {
		_, _ = fmt.Fprintln(w, styles.Title.Render(n.title.val))
	}
	if n.description.val != "" {
		_, _ = fmt.Fprintln(w, n.description.val)
	}
	return nil
}

// WithTheme sets the theme on a note field.
func (n *Note) WithTheme(theme Theme) Field {
	if n.theme != nil {
		return n
	}
	n.theme = theme
	return n
}

// WithKeyMap sets the keymap on a note field.
func (n *Note) WithKeyMap(k *KeyMap) Field {
	n.keymap = k.Note
	return n
}

// WithWidth sets the width of the note field.
func (n *Note) WithWidth(width int) Field {
	n.width = width
	return n
}

// WithHeight sets the height of the note field.
func (n *Note) WithHeight(height int) Field {
	n.Height(height)
	return n
}

// WithPosition sets the position information of the note field.
func (n *Note) WithPosition(p FieldPosition) Field {
	// if the note is the only field on the screen,
	// we shouldn't skip the entire group.
	if p.Field == p.FirstField && p.Field == p.LastField {
		n.skip = false
	}
	n.keymap.Prev.SetEnabled(!p.IsFirst())
	n.keymap.Next.SetEnabled(!p.IsLast())
	n.keymap.Submit.SetEnabled(p.IsLast())
	return n
}

// GetValue satisfies the Field interface, notes do not have values.
func (n *Note) GetValue() any { return nil }

// GetKey satisfies the Field interface, notes do not have keys.
func (n *Note) GetKey() string { return "" }

func render(input string) string {
	var result strings.Builder
	var italic, bold, codeblock bool
	var escape bool

	for _, char := range input {
		if escape || codeblock {
			result.WriteRune(char)
			escape = false
			continue
		}
		switch char {
		case '\\':
			escape = true
		case '_':
			if !italic {
				result.WriteString("\033[3m")
				italic = true
			} else {
				result.WriteString("\033[23m")
				italic = false
			}
		case '*':
			if !bold {
				result.WriteString("\033[1m")
				bold = true
			} else {
				result.WriteString("\033[22m")
				bold = false
			}
		case '`':
			if !codeblock {
				result.WriteString("\033[0;37;40m")
				result.WriteString(" ")
				codeblock = true
			} else {
				result.WriteString(" ")
				result.WriteString("\033[0m")
				codeblock = false

				if bold {
					result.WriteString("\033[1m")
				}
				if italic {
					result.WriteString("\033[3m")
				}
			}
		default:
			result.WriteRune(char)
		}
	}

	// Reset any open formatting
	result.WriteString("\033[0m")

	return result.String()
}


================================================
FILE: field_select.go
================================================
package huh

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

	"charm.land/bubbles/v2/key"
	"charm.land/bubbles/v2/spinner"
	"charm.land/bubbles/v2/textinput"
	"charm.land/bubbles/v2/viewport"
	tea "charm.land/bubbletea/v2"
	"charm.land/huh/v2/internal/accessibility"
	"charm.land/lipgloss/v2"
	"github.com/charmbracelet/x/exp/ordered"
)

const (
	minHeight     = 1
	defaultHeight = 10
)

// Select is a select field.
//
// A select field is a field that allows the user to select from a list of
// options. The options can be provided statically or dynamically using Options
// or OptionsFunc. The options can be filtered using "/" and navigation is done
// using j/k, up/down, or ctrl+n/ctrl+p keys.
type Select[T comparable] struct {
	id       int
	accessor Accessor[T]
	key      string

	viewport viewport.Model

	title           Eval[string]
	description     Eval[string]
	options         Eval[[]Option[T]]
	filteredOptions []Option[T]

	validate func(T) error
	err      error

	selected  int
	focused   bool
	filtering bool
	filter    textinput.Model
	spinner   spinner.Model

	inline    bool
	width     int
	height    int
	theme     Theme
	hasDarkBg bool
	keymap    SelectKeyMap
}

// NewSelect creates a new select field.
//
// A select field is a field that allows the user to select from a list of
// options. The options can be provided statically or dynamically using Options
// or OptionsFunc. The options can be filtered using "/" and navigation is done
// using j/k, up/down, or ctrl+n/ctrl+p keys.
func NewSelect[T comparable]() *Select[T] {
	filter := textinput.New()
	filter.Prompt = "/"

	s := spinner.New(spinner.WithSpinner(spinner.Line))

	return &Select[T]{
		accessor:    &EmbeddedAccessor[T]{},
		validate:    func(T) error { return nil },
		filtering:   false,
		filter:      filter,
		id:          nextID(),
		options:     Eval[[]Option[T]]{cache: make(map[uint64][]Option[T])},
		title:       Eval[string]{cache: make(map[uint64]string)},
		description: Eval[string]{cache: make(map[uint64]string)},
		spinner:     s,
	}
}

// Value sets the value of the select field.
func (s *Select[T]) Value(value *T) *Select[T] {
	return s.Accessor(NewPointerAccessor(value))
}

// Accessor sets the accessor of the select field.
func (s *Select[T]) Accessor(accessor Accessor[T]) *Select[T] {
	s.accessor = accessor
	s.selectValue(s.accessor.Get())
	s.updateValue()
	return s
}

func (s *Select[T]) selectValue(value T) {
	for i, o := range s.options.val {
		if o.Value == value {
			s.selected = i
			break
		}
	}
}

// Key sets the key of the select field which can be used to retrieve the value
// after submission.
func (s *Select[T]) Key(key string) *Select[T] {
	s.key = key
	return s
}

// Title sets the title of the select field.
//
// This title will be static, for dynamic titles use `TitleFunc`.
func (s *Select[T]) Title(title string) *Select[T] {
	s.title.val = title
	s.title.fn = nil
	return s
}

// TitleFunc sets the title func of the select field.
//
// This TitleFunc will be re-evaluated when the binding of the TitleFunc
// changes. This when you want to display dynamic content and update the title
// when another part of your form changes.
//
// See README#Dynamic for more usage information.
func (s *Select[T]) TitleFunc(f func() string, bindings any) *Select[T] {
	s.title.fn = f
	s.title.bindings = bindings
	return s
}

// Filtering sets the filtering state of the select field.
func (s *Select[T]) Filtering(filtering bool) *Select[T] {
	s.filtering = filtering
	s.filter.Focus()
	return s
}

// Description sets the description of the select field.
//
// This description will be static, for dynamic descriptions use `DescriptionFunc`.
func (s *Select[T]) Description(description string) *Select[T] {
	s.description.val = description
	return s
}

// DescriptionFunc sets the description func of the select field.
//
// This DescriptionFunc will be re-evaluated when the binding of the
// DescriptionFunc changes. This is useful when you want to display dynamic
// content and update the description when another part of your form changes.
//
// See README#Dynamic for more usage information.
func (s *Select[T]) DescriptionFunc(f func() string, bindings any) *Select[T] {
	s.description.fn = f
	s.description.bindings = bindings
	return s
}

// Options sets the options of the select field.
//
// This is what your user will select from.
//
// Title
// Description
//
//	-> Option 1
//	   Option 2
//	   Option 3
//
// These options will be static, for dynamic options use `OptionsFunc`.
func (s *Select[T]) Options(options ...Option[T]) *Select[T] {
	if len(options) <= 0 {
		return s
	}
	s.options.val = options
	s.filteredOptions = options

	s.selectOption()

	s.updateViewportSize()
	s.updateValue()

	return s
}

func (s *Select[T]) selectOption() {
	// Set the cursor to the existing value or the last selected option.
	for i, option := range s.options.val {
		if option.Value == s.accessor.Get() {
			s.selected = i
			break
		}
		if option.selected {
			s.selected = i
			break
		}
	}
	s.ensureCursorVisible()
}

// OptionsFunc sets the options func of the select field.
//
// This OptionsFunc will be re-evaluated when the binding of the OptionsFunc
// changes. This is useful when you want to display dynamic content and update
// the options when another part of your form changes.
//
// For example, changing the state / provinces, based on the selected country.
//
//	   huh.NewSelect[string]().
//		    Options(huh.NewOptions("United States", "Canada", "Mexico")...).
//		    Value(&country).
//		    Title("Country").
//		    Height(
Download .txt
gitextract_o580ujl4/

├── .gitattributes
├── .github/
│   ├── CODEOWNERS
│   ├── dependabot.yml
│   └── workflows/
│       ├── build.yml
│       ├── dependabot-sync.yml
│       ├── lint-sync.yml
│       ├── lint.yml
│       └── release.yml
├── .gitignore
├── .golangci.yml
├── .goreleaser.yml
├── LICENSE
├── Makefile
├── README.md
├── UPGRADE_GUIDE_V2.md
├── accessor.go
├── eval.go
├── examples/
│   ├── .gitignore
│   ├── accessibility/
│   │   ├── accessible.tape
│   │   └── main.go
│   ├── accessibility-secure-input/
│   │   └── main.go
│   ├── bubbletea/
│   │   ├── demo.tape
│   │   └── main.go
│   ├── bubbletea-options/
│   │   └── main.go
│   ├── burger/
│   │   ├── demo.tape
│   │   └── main.go
│   ├── conditional/
│   │   └── main.go
│   ├── dynamic/
│   │   ├── demo.tape
│   │   ├── dynamic-all/
│   │   │   └── main.go
│   │   ├── dynamic-bubbletea/
│   │   │   └── main.go
│   │   ├── dynamic-count/
│   │   │   └── main.go
│   │   ├── dynamic-country/
│   │   │   └── main.go
│   │   ├── dynamic-increment/
│   │   │   └── main.go
│   │   ├── dynamic-markdown/
│   │   │   └── main.go
│   │   ├── dynamic-name/
│   │   │   └── main.go
│   │   └── dynamic-suggestions/
│   │       └── main.go
│   ├── filepicker/
│   │   ├── artichoke.hs
│   │   ├── demo.tape
│   │   └── main.go
│   ├── filepicker-picking/
│   │   └── main.go
│   ├── gh/
│   │   └── create.go
│   ├── git/
│   │   └── main.go
│   ├── go.mod
│   ├── go.sum
│   ├── gum/
│   │   └── main.go
│   ├── help/
│   │   └── main.go
│   ├── hide/
│   │   ├── hide.tape
│   │   └── main.go
│   ├── layout/
│   │   ├── columns/
│   │   │   └── main.go
│   │   ├── default/
│   │   │   └── main.go
│   │   ├── grid/
│   │   │   └── main.go
│   │   └── stack/
│   │       └── main.go
│   ├── multiple-groups/
│   │   └── main.go
│   ├── readme/
│   │   ├── confirm/
│   │   │   ├── confirm.tape
│   │   │   └── main.go
│   │   ├── input/
│   │   │   ├── input.tape
│   │   │   ├── main.go
│   │   │   └── suggestions.tape
│   │   ├── main/
│   │   │   └── main.go
│   │   ├── multiselect/
│   │   │   ├── main.go
│   │   │   └── multiselect.tape
│   │   ├── note/
│   │   │   └── main.go
│   │   ├── select/
│   │   │   ├── main.go
│   │   │   ├── scroll/
│   │   │   │   ├── scroll.go
│   │   │   │   └── scroll.tape
│   │   │   └── select.tape
│   │   └── text/
│   │       ├── main.go
│   │       └── text.tape
│   ├── scroll/
│   │   └── main.go
│   ├── skip/
│   │   └── main.go
│   ├── spinner/
│   │   ├── accessible/
│   │   │   └── main.go
│   │   ├── context/
│   │   │   └── main.go
│   │   ├── context-and-action/
│   │   │   └── main.go
│   │   ├── context-and-action-and-error/
│   │   │   └── main.go
│   │   ├── loading/
│   │   │   ├── demo.tape
│   │   │   └── main.go
│   │   └── static/
│   │       └── main.go
│   ├── ssh-form/
│   │   └── main.go
│   ├── stickers/
│   │   └── main.go
│   ├── theme/
│   │   ├── main.go
│   │   └── theme.tape
│   └── timer/
│       └── main.go
├── field_confirm.go
├── field_filepicker.go
├── field_input.go
├── field_multiselect.go
├── field_note.go
├── field_select.go
├── field_text.go
├── form.go
├── go.mod
├── go.sum
├── group.go
├── huh.go
├── huh_test.go
├── internal/
│   ├── accessibility/
│   │   └── accessibility.go
│   ├── compat/
│   │   └── model.go
│   └── selector/
│       └── selector.go
├── keymap.go
├── layout.go
├── option.go
├── run.go
├── spinner/
│   ├── spinner.go
│   └── spinner_test.go
├── theme.go
├── validate.go
├── wrap.go
└── zz_resize_width_test.go
Download .txt
SYMBOL INDEX (687 symbols across 71 files)

FILE: accessor.go
  type Accessor (line 4) | type Accessor interface
  type EmbeddedAccessor (line 10) | type EmbeddedAccessor struct
  method Get (line 15) | func (a *EmbeddedAccessor[T]) Get() T {
  method Set (line 20) | func (a *EmbeddedAccessor[T]) Set(value T) {
  type PointerAccessor (line 25) | type PointerAccessor struct
  function NewPointerAccessor (line 30) | func NewPointerAccessor[T any](value *T) *PointerAccessor[T] {
  method Get (line 37) | func (a *PointerAccessor[T]) Get() T {
  method Set (line 42) | func (a *PointerAccessor[T]) Set(value T) {

FILE: eval.go
  type Eval (line 14) | type Eval struct
  constant spinnerShowThreshold (line 26) | spinnerShowThreshold = 25 * time.Millisecond
  function hash (line 28) | func hash(val any) uint64 {
  method shouldUpdate (line 33) | func (e *Eval[T]) shouldUpdate() (bool, uint64) {
  method loadFromCache (line 41) | func (e *Eval[T]) loadFromCache() bool {
  method update (line 50) | func (e *Eval[T]) update(val T) {
  type updateTitleMsg (line 56) | type updateTitleMsg struct
  type updateDescriptionMsg (line 62) | type updateDescriptionMsg struct
  type updatePlaceholderMsg (line 68) | type updatePlaceholderMsg struct
  type updateSuggestionsMsg (line 74) | type updateSuggestionsMsg struct
  type updateOptionsMsg (line 80) | type updateOptionsMsg struct

FILE: examples/accessibility-secure-input/main.go
  function validate (line 10) | func validate(s string) error {
  function main (line 17) | func main() {

FILE: examples/accessibility/main.go
  function main (line 9) | func main() {

FILE: examples/bubbletea-options/main.go
  function main (line 10) | func main() {

FILE: examples/bubbletea/main.go
  constant maxWidth (line 14) | maxWidth = 80
  type Styles (line 16) | type Styles struct
  function NewStyles (line 28) | func NewStyles(hasDarkBg bool) *Styles {
  type state (line 60) | type state
  constant statusNormal (line 63) | statusNormal state = iota
  constant stateDone (line 64) | stateDone
  type Model (line 67) | type Model struct
    method Init (line 114) | func (m Model) Init() tea.Cmd {
    method Update (line 125) | func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    method View (line 158) | func (m Model) View() tea.View {
    method errorView (line 228) | func (m Model) errorView() string {
    method appBoundaryView (line 236) | func (m Model) appBoundaryView(text string) string {
    method appErrorBoundaryView (line 247) | func (m Model) appErrorBoundaryView(text string) string {
    method getRole (line 258) | func (m Model) getRole() (string, string) {
  function NewModel (line 75) | func NewModel() Model {
  function min (line 118) | func min(x, y int) int {
  function main (line 293) | func main() {

FILE: examples/burger/main.go
  type Spice (line 17) | type Spice
    method String (line 25) | func (s Spice) String() string {
  constant Mild (line 20) | Mild Spice = iota + 1
  constant Medium (line 21) | Medium
  constant Hot (line 22) | Hot
  type Order (line 38) | type Order struct
  type Burger (line 46) | type Burger struct
  function main (line 52) | func main() {

FILE: examples/conditional/main.go
  type consumable (line 10) | type consumable
    method String (line 18) | func (c consumable) String() string {
  constant fruits (line 13) | fruits consumable = iota
  constant vegetables (line 14) | vegetables
  constant drinks (line 15) | drinks
  function main (line 22) | func main() {

FILE: examples/dynamic/dynamic-all/main.go
  function main (line 10) | func main() {

FILE: examples/dynamic/dynamic-bubbletea/main.go
  constant maxWidth (line 14) | maxWidth = 80
  type Styles (line 16) | type Styles struct
  function NewStyles (line 28) | func NewStyles(hasDarkBg bool) *Styles {
  type state (line 60) | type state
  constant statusNormal (line 63) | statusNormal state = iota
  constant stateDone (line 64) | stateDone
  type Model (line 67) | type Model struct
    method Init (line 122) | func (m Model) Init() tea.Cmd {
    method Update (line 133) | func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    method View (line 164) | func (m Model) View() tea.View {
    method errorView (line 234) | func (m Model) errorView() string {
    method appBoundaryView (line 242) | func (m Model) appBoundaryView(text string) string {
    method appErrorBoundaryView (line 253) | func (m Model) appErrorBoundaryView(text string) string {
    method getRole (line 264) | func (m Model) getRole() (string, string) {
  function NewModel (line 75) | func NewModel() Model {
  function min (line 126) | func min(x, y int) int {
  function main (line 299) | func main() {

FILE: examples/dynamic/dynamic-count/main.go
  function main (line 12) | func main() {

FILE: examples/dynamic/dynamic-country/main.go
  function main (line 11) | func main() {

FILE: examples/dynamic/dynamic-increment/main.go
  function main (line 10) | func main() {

FILE: examples/dynamic/dynamic-markdown/main.go
  function main (line 10) | func main() {

FILE: examples/dynamic/dynamic-name/main.go
  function main (line 10) | func main() {

FILE: examples/dynamic/dynamic-suggestions/main.go
  function main (line 11) | func main() {

FILE: examples/filepicker-picking/main.go
  function main (line 9) | func main() {

FILE: examples/filepicker/main.go
  function main (line 7) | func main() {

FILE: examples/gh/create.go
  type Action (line 13) | type Action
  constant Cancel (line 16) | Cancel Action = iota
  constant Push (line 17) | Push
  constant Fork (line 18) | Fork
  constant Skip (line 19) | Skip
  function customTheme (line 24) | func customTheme(isDark bool) *huh.Styles {
  function main (line 31) | func main() {

FILE: examples/git/main.go
  function main (line 14) | func main() {

FILE: examples/gum/main.go
  function main (line 10) | func main() {

FILE: examples/help/main.go
  function main (line 5) | func main() {

FILE: examples/hide/main.go
  function main (line 9) | func main() {

FILE: examples/layout/columns/main.go
  function main (line 5) | func main() {

FILE: examples/layout/default/main.go
  function main (line 5) | func main() {

FILE: examples/layout/grid/main.go
  function main (line 5) | func main() {

FILE: examples/layout/stack/main.go
  function main (line 5) | func main() {

FILE: examples/multiple-groups/main.go
  function main (line 10) | func main() {

FILE: examples/readme/confirm/main.go
  function main (line 7) | func main() {

FILE: examples/readme/input/main.go
  function isFood (line 9) | func isFood(_ string) error {
  function main (line 13) | func main() {

FILE: examples/readme/main/main.go
  function checkForPlagiarism (line 10) | func checkForPlagiarism(s string) error { return nil }
  function isFood (line 13) | func isFood(s string) error { return nil }
  function validateName (line 16) | func validateName(s string) error { return nil }
  function main (line 18) | func main() {

FILE: examples/readme/multiselect/main.go
  function main (line 5) | func main() {

FILE: examples/readme/note/main.go
  function main (line 5) | func main() {

FILE: examples/readme/select/main.go
  function main (line 5) | func main() {

FILE: examples/readme/select/scroll/scroll.go
  type Pokemon (line 5) | type Pokemon struct
    method String (line 41) | func (p Pokemon) String() string {
  function main (line 45) | func main() {

FILE: examples/readme/text/main.go
  function checkForPlagiarism (line 6) | func checkForPlagiarism(s string) error { return nil }
  function main (line 8) | func main() {

FILE: examples/scroll/main.go
  function main (line 5) | func main() {

FILE: examples/skip/main.go
  function main (line 7) | func main() {

FILE: examples/spinner/accessible/main.go
  function main (line 11) | func main() {

FILE: examples/spinner/context-and-action-and-error/main.go
  function main (line 12) | func main() {

FILE: examples/spinner/context-and-action/main.go
  function main (line 13) | func main() {

FILE: examples/spinner/context/main.go
  function main (line 11) | func main() {

FILE: examples/spinner/loading/main.go
  function main (line 11) | func main() {

FILE: examples/spinner/static/main.go
  function main (line 9) | func main() {

FILE: examples/ssh-form/main.go
  constant host (line 24) | host = "localhost"
  constant port (line 25) | port = "2222"
  function main (line 28) | func main() {
  function customTheme (line 62) | func customTheme(hasDarkBg bool) *huh.Styles {
  function teaHandler (line 80) | func teaHandler(s ssh.Session) (tea.Model, []tea.ProgramOption) {
  type model (line 96) | type model struct
    method Init (line 102) | func (m model) Init() tea.Cmd { return m.form.Init() }
    method Update (line 104) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    method View (line 129) | func (m model) View() tea.View {

FILE: examples/stickers/main.go
  function main (line 7) | func main() {

FILE: examples/theme/main.go
  function main (line 18) | func main() {

FILE: examples/timer/main.go
  type mode (line 29) | type mode
  constant Initial (line 32) | Initial mode = iota
  constant Focusing (line 33) | Focusing
  constant Paused (line 34) | Paused
  constant Breaking (line 35) | Breaking
  type Model (line 38) | type Model struct
    method Init (line 53) | func (m Model) Init() tea.Cmd {
    method Update (line 65) | func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    method View (line 131) | func (m Model) View() tea.View {
  constant tickInterval (line 57) | tickInterval = time.Second / 2
  type tickMsg (line 59) | type tickMsg
  function tickCmd (line 61) | func tickCmd(t time.Time) tea.Msg {
  function customTheme (line 168) | func customTheme(isDark bool) *huh.Styles {
  function NewModel (line 178) | func NewModel() Model {
  function main (line 214) | func main() {

FILE: field_confirm.go
  type Confirm (line 15) | type Confirm struct
    method Validate (line 58) | func (c *Confirm) Validate(validate func(bool) error) *Confirm {
    method Error (line 64) | func (c *Confirm) Error() error {
    method Skip (line 69) | func (*Confirm) Skip() bool {
    method Zoom (line 74) | func (*Confirm) Zoom() bool {
    method Affirmative (line 79) | func (c *Confirm) Affirmative(affirmative string) *Confirm {
    method Negative (line 85) | func (c *Confirm) Negative(negative string) *Confirm {
    method Value (line 91) | func (c *Confirm) Value(value *bool) *Confirm {
    method Accessor (line 96) | func (c *Confirm) Accessor(accessor Accessor[bool]) *Confirm {
    method Key (line 102) | func (c *Confirm) Key(key string) *Confirm {
    method Title (line 108) | func (c *Confirm) Title(title string) *Confirm {
    method TitleFunc (line 115) | func (c *Confirm) TitleFunc(f func() string, bindings any) *Confirm {
    method Description (line 122) | func (c *Confirm) Description(description string) *Confirm {
    method DescriptionFunc (line 129) | func (c *Confirm) DescriptionFunc(f func() string, bindings any) *Conf...
    method Inline (line 136) | func (c *Confirm) Inline(inline bool) *Confirm {
    method Focus (line 142) | func (c *Confirm) Focus() tea.Cmd {
    method Blur (line 148) | func (c *Confirm) Blur() tea.Cmd {
    method KeyBinds (line 155) | func (c *Confirm) KeyBinds() []key.Binding {
    method Init (line 160) | func (c *Confirm) Init() tea.Cmd {
    method Update (line 165) | func (c *Confirm) Update(msg tea.Msg) (Model, tea.Cmd) {
    method activeStyles (line 225) | func (c *Confirm) activeStyles() *FieldStyles {
    method View (line 237) | func (c *Confirm) View() string {
    method Run (line 299) | func (c *Confirm) Run() error {
    method RunAccessible (line 304) | func (c *Confirm) RunAccessible(w io.Writer, r io.Reader) error {
    method String (line 318) | func (c *Confirm) String() string {
    method WithTheme (line 326) | func (c *Confirm) WithTheme(theme Theme) Field {
    method WithKeyMap (line 335) | func (c *Confirm) WithKeyMap(k *KeyMap) Field {
    method WithWidth (line 341) | func (c *Confirm) WithWidth(width int) Field {
    method WithHeight (line 347) | func (c *Confirm) WithHeight(height int) Field {
    method WithPosition (line 353) | func (c *Confirm) WithPosition(p FieldPosition) Field {
    method WithButtonAlignment (line 361) | func (c *Confirm) WithButtonAlignment(p lipgloss.Position) *Confirm {
    method GetKey (line 367) | func (c *Confirm) GetKey() string {
    method GetValue (line 372) | func (c *Confirm) GetValue() any {
  function NewConfirm (line 44) | func NewConfirm() *Confirm {

FILE: field_filepicker.go
  type FilePicker (line 20) | type FilePicker struct
    method CurrentDirectory (line 62) | func (f *FilePicker) CurrentDirectory(directory string) *FilePicker {
    method Cursor (line 71) | func (f *FilePicker) Cursor(cursor string) *FilePicker {
    method Picking (line 77) | func (f *FilePicker) Picking(v bool) *FilePicker {
    method ShowHidden (line 83) | func (f *FilePicker) ShowHidden(v bool) *FilePicker {
    method ShowSize (line 89) | func (f *FilePicker) ShowSize(v bool) *FilePicker {
    method ShowPermissions (line 95) | func (f *FilePicker) ShowPermissions(v bool) *FilePicker {
    method FileAllowed (line 101) | func (f *FilePicker) FileAllowed(v bool) *FilePicker {
    method DirAllowed (line 107) | func (f *FilePicker) DirAllowed(v bool) *FilePicker {
    method Value (line 113) | func (f *FilePicker) Value(value *string) *FilePicker {
    method Accessor (line 118) | func (f *FilePicker) Accessor(accessor Accessor[string]) *FilePicker {
    method Key (line 125) | func (f *FilePicker) Key(key string) *FilePicker {
    method Title (line 131) | func (f *FilePicker) Title(title string) *FilePicker {
    method Description (line 137) | func (f *FilePicker) Description(description string) *FilePicker {
    method AllowedTypes (line 144) | func (f *FilePicker) AllowedTypes(types []string) *FilePicker {
    method Height (line 151) | func (f *FilePicker) Height(height int) *FilePicker {
    method Validate (line 157) | func (f *FilePicker) Validate(validate func(string) error) *FilePicker {
    method Error (line 163) | func (f *FilePicker) Error() error {
    method Skip (line 168) | func (*FilePicker) Skip() bool {
    method Zoom (line 173) | func (f *FilePicker) Zoom() bool {
    method Focus (line 178) | func (f *FilePicker) Focus() tea.Cmd {
    method Blur (line 184) | func (f *FilePicker) Blur() tea.Cmd {
    method KeyBinds (line 192) | func (f *FilePicker) KeyBinds() []key.Binding {
    method Init (line 197) | func (f *FilePicker) Init() tea.Cmd {
    method Update (line 202) | func (f *FilePicker) Update(msg tea.Msg) (Model, tea.Cmd) {
    method activeStyles (line 245) | func (f *FilePicker) activeStyles() *FieldStyles {
    method renderTitle (line 256) | func (f *FilePicker) renderTitle() string {
    method renderDescription (line 262) | func (f FilePicker) renderDescription() string {
    method View (line 269) | func (f *FilePicker) View() string {
    method pickerView (line 283) | func (f *FilePicker) pickerView() string {
    method setPicking (line 294) | func (f *FilePicker) setPicking(v bool) {
    method Run (line 313) | func (f *FilePicker) Run() error {
    method RunAccessible (line 318) | func (f *FilePicker) RunAccessible(w io.Writer, r io.Reader) error {
    method WithTheme (line 363) | func (f *FilePicker) WithTheme(theme Theme) Field {
    method WithKeyMap (line 389) | func (f *FilePicker) WithKeyMap(k *KeyMap) Field {
    method WithWidth (line 407) | func (f *FilePicker) WithWidth(width int) Field {
    method WithHeight (line 413) | func (f *FilePicker) WithHeight(height int) Field {
    method WithPosition (line 430) | func (f *FilePicker) WithPosition(p FieldPosition) Field {
    method GetKey (line 438) | func (f *FilePicker) GetKey() string {
    method GetValue (line 443) | func (f *FilePicker) GetValue() any {
  function NewFilePicker (line 46) | func NewFilePicker() *FilePicker {
  constant fileSizeWidth (line 358) | fileSizeWidth = 7
  constant paddingLeft (line 359) | paddingLeft   = 2

FILE: field_input.go
  type Input (line 23) | type Input struct
    method Value (line 72) | func (i *Input) Value(value *string) *Input {
    method Accessor (line 77) | func (i *Input) Accessor(accessor Accessor[string]) *Input {
    method Key (line 84) | func (i *Input) Key(key string) *Input {
    method Title (line 92) | func (i *Input) Title(title string) *Input {
    method Description (line 101) | func (i *Input) Description(description string) *Input {
    method TitleFunc (line 114) | func (i *Input) TitleFunc(f func() string, bindings any) *Input {
    method DescriptionFunc (line 127) | func (i *Input) DescriptionFunc(f func() string, bindings any) *Input {
    method Prompt (line 134) | func (i *Input) Prompt(prompt string) *Input {
    method CharLimit (line 140) | func (i *Input) CharLimit(charlimit int) *Input {
    method Suggestions (line 149) | func (i *Input) Suggestions(suggestions []string) *Input {
    method SuggestionsFunc (line 166) | func (i *Input) SuggestionsFunc(f func() []string, bindings any) *Input {
    method EchoMode (line 194) | func (i *Input) EchoMode(mode EchoMode) *Input {
    method Password (line 202) | func (i *Input) Password(password bool) *Input {
    method Placeholder (line 212) | func (i *Input) Placeholder(str string) *Input {
    method PlaceholderFunc (line 218) | func (i *Input) PlaceholderFunc(f func() string, bindings any) *Input {
    method Inline (line 225) | func (i *Input) Inline(inline bool) *Input {
    method Validate (line 231) | func (i *Input) Validate(validate func(string) error) *Input {
    method Error (line 237) | func (i *Input) Error() error { return i.err }
    method Skip (line 240) | func (*Input) Skip() bool { return false }
    method Zoom (line 243) | func (*Input) Zoom() bool { return false }
    method Focus (line 246) | func (i *Input) Focus() tea.Cmd {
    method Blur (line 252) | func (i *Input) Blur() tea.Cmd {
    method KeyBinds (line 261) | func (i *Input) KeyBinds() []key.Binding {
    method Init (line 269) | func (i *Input) Init() tea.Cmd {
    method Update (line 275) | func (i *Input) Update(msg tea.Msg) (Model, tea.Cmd) {
    method activeStyles (line 368) | func (i *Input) activeStyles() *FieldStyles {
    method View (line 380) | func (i *Input) View() string {
    method Run (line 421) | func (i *Input) Run() error {
    method run (line 426) | func (i *Input) run() error {
    method RunAccessible (line 431) | func (i *Input) RunAccessible(w io.Writer, r io.Reader) error {
    method WithKeyMap (line 465) | func (i *Input) WithKeyMap(k *KeyMap) Field {
    method WithTheme (line 472) | func (i *Input) WithTheme(theme Theme) Field {
    method WithWidth (line 481) | func (i *Input) WithWidth(width int) Field {
    method WithHeight (line 496) | func (i *Input) WithHeight(height int) Field {
    method WithPosition (line 502) | func (i *Input) WithPosition(p FieldPosition) Field {
    method GetKey (line 510) | func (i *Input) GetKey() string { return i.key }
    method GetValue (line 513) | func (i *Input) GetValue() any {
  function NewInput (line 54) | func NewInput() *Input {
  type EchoMode (line 177) | type EchoMode
  constant EchoModeNormal (line 182) | EchoModeNormal EchoMode = EchoMode(textinput.EchoNormal)
  constant EchoModePassword (line 186) | EchoModePassword EchoMode = EchoMode(textinput.EchoPassword)
  constant EchoModeNone (line 190) | EchoModeNone EchoMode = EchoMode(textinput.EchoNone)

FILE: field_multiselect.go
  type MultiSelect (line 22) | type MultiSelect struct
  function NewMultiSelect (line 56) | func NewMultiSelect[T comparable]() *MultiSelect[T] {
  method Value (line 77) | func (m *MultiSelect[T]) Value(value *[]T) *MultiSelect[T] {
  method Accessor (line 82) | func (m *MultiSelect[T]) Accessor(accessor Accessor[[]T]) *MultiSelect[T] {
  method Key (line 94) | func (m *MultiSelect[T]) Key(key string) *MultiSelect[T] {
  method Title (line 100) | func (m *MultiSelect[T]) Title(title string) *MultiSelect[T] {
  method TitleFunc (line 107) | func (m *MultiSelect[T]) TitleFunc(f func() string, bindings any) *Multi...
  method Description (line 114) | func (m *MultiSelect[T]) Description(description string) *MultiSelect[T] {
  method DescriptionFunc (line 120) | func (m *MultiSelect[T]) DescriptionFunc(f func() string, bindings any) ...
  method Options (line 127) | func (m *MultiSelect[T]) Options(options ...Option[T]) *MultiSelect[T] {
  method selectOptions (line 139) | func (m *MultiSelect[T]) selectOptions() {
  method OptionsFunc (line 160) | func (m *MultiSelect[T]) OptionsFunc(f func() []Option[T], bindings any)...
  method Filterable (line 177) | func (m *MultiSelect[T]) Filterable(filterable bool) *MultiSelect[T] {
  method Filtering (line 183) | func (m *MultiSelect[T]) Filtering(filtering bool) *MultiSelect[T] {
  method Limit (line 190) | func (m *MultiSelect[T]) Limit(limit int) *MultiSelect[T] {
  method Width (line 197) | func (m *MultiSelect[T]) Width(width int) *MultiSelect[T] {
  method Height (line 206) | func (m *MultiSelect[T]) Height(height int) *MultiSelect[T] {
  method Validate (line 215) | func (m *MultiSelect[T]) Validate(validate func([]T) error) *MultiSelect...
  method Error (line 221) | func (m *MultiSelect[T]) Error() error {
  method Skip (line 226) | func (*MultiSelect[T]) Skip() bool {
  method Zoom (line 231) | func (*MultiSelect[T]) Zoom() bool {
  method Focus (line 236) | func (m *MultiSelect[T]) Focus() tea.Cmd {
  method Blur (line 243) | func (m *MultiSelect[T]) Blur() tea.Cmd {
  method Hovered (line 252) | func (m *MultiSelect[T]) Hovered() (T, bool) {
  method KeyBinds (line 261) | func (m *MultiSelect[T]) KeyBinds() []key.Binding {
  method Init (line 288) | func (m *MultiSelect[T]) Init() tea.Cmd {
  method Update (line 293) | func (m *MultiSelect[T]) Update(msg tea.Msg) (Model, tea.Cmd) {
  method updateViewportSize (line 495) | func (m *MultiSelect[T]) updateViewportSize() {
  method numSelected (line 518) | func (m *MultiSelect[T]) numSelected() int {
  method numFilteredSelected (line 530) | func (m *MultiSelect[T]) numFilteredSelected() int {
  method updateValue (line 540) | func (m *MultiSelect[T]) updateValue() {
  method activeStyles (line 551) | func (m *MultiSelect[T]) activeStyles() *FieldStyles {
  method titleView (line 562) | func (m *MultiSelect[T]) titleView() string {
  method descriptionView (line 585) | func (m *MultiSelect[T]) descriptionView() string {
  method renderOption (line 593) | func (m *MultiSelect[T]) renderOption(option Option[T], cursor, selected...
  method cursorLineOffset (line 613) | func (m *MultiSelect[T]) cursorLineOffset() (offset int, height int) {
  method ensureCursorVisible (line 628) | func (m *MultiSelect[T]) ensureCursorVisible() {
  method optionsView (line 633) | func (m *MultiSelect[T]) optionsView() (string, int, int) {
  method View (line 667) | func (m *MultiSelect[T]) View() string {
  method printOptions (line 686) | func (m *MultiSelect[T]) printOptions(w io.Writer) {
  method setFilter (line 702) | func (m *MultiSelect[T]) setFilter(filter bool) {
  method filterFunc (line 713) | func (m *MultiSelect[T]) filterFunc(option string) bool {
  method setSelectAllHelp (line 719) | func (m *MultiSelect[T]) setSelectAllHelp() {
  method Run (line 734) | func (m *MultiSelect[T]) Run() error {
  method RunAccessible (line 739) | func (m *MultiSelect[T]) RunAccessible(w io.Writer, r io.Reader) error {
  method WithTheme (line 780) | func (m *MultiSelect[T]) WithTheme(theme Theme) Field {
  method WithKeyMap (line 799) | func (m *MultiSelect[T]) WithKeyMap(k *KeyMap) Field {
  method WithWidth (line 810) | func (m *MultiSelect[T]) WithWidth(width int) Field {
  method WithHeight (line 818) | func (m *MultiSelect[T]) WithHeight(height int) Field {
  method WithPosition (line 824) | func (m *MultiSelect[T]) WithPosition(p FieldPosition) Field {
  method GetKey (line 835) | func (m *MultiSelect[T]) GetKey() string {
  method GetValue (line 840) | func (m *MultiSelect[T]) GetValue() any {
  method GetFiltering (line 845) | func (m *MultiSelect[T]) GetFiltering() bool {

FILE: field_note.go
  type Note (line 17) | type Note struct
    method Title (line 55) | func (n *Note) Title(title string) *Note {
    method TitleFunc (line 68) | func (n *Note) TitleFunc(f func() string, bindings any) *Note {
    method Description (line 77) | func (n *Note) Description(description string) *Note {
    method DescriptionFunc (line 101) | func (n *Note) DescriptionFunc(f func() string, bindings any) *Note {
    method Height (line 108) | func (n *Note) Height(height int) *Note {
    method Next (line 119) | func (n *Note) Next(show bool) *Note {
    method NextLabel (line 125) | func (n *Note) NextLabel(label string) *Note {
    method Focus (line 131) | func (n *Note) Focus() tea.Cmd {
    method Blur (line 137) | func (n *Note) Blur() tea.Cmd {
    method Error (line 143) | func (n *Note) Error() error { return nil }
    method Skip (line 146) | func (n *Note) Skip() bool { return n.skip }
    method Zoom (line 149) | func (n *Note) Zoom() bool { return false }
    method KeyBinds (line 152) | func (n *Note) KeyBinds() []key.Binding {
    method Init (line 161) | func (n *Note) Init() tea.Cmd { return nil }
    method Update (line 164) | func (n *Note) Update(msg tea.Msg) (Model, tea.Cmd) {
    method activeStyles (line 209) | func (n *Note) activeStyles() *FieldStyles {
    method View (line 221) | func (n *Note) View() string {
    method Run (line 245) | func (n *Note) Run() error {
    method RunAccessible (line 250) | func (n *Note) RunAccessible(w io.Writer, _ io.Reader) error {
    method WithTheme (line 262) | func (n *Note) WithTheme(theme Theme) Field {
    method WithKeyMap (line 271) | func (n *Note) WithKeyMap(k *KeyMap) Field {
    method WithWidth (line 277) | func (n *Note) WithWidth(width int) Field {
    method WithHeight (line 283) | func (n *Note) WithHeight(height int) Field {
    method WithPosition (line 289) | func (n *Note) WithPosition(p FieldPosition) Field {
    method GetValue (line 302) | func (n *Note) GetValue() any { return nil }
    method GetKey (line 305) | func (n *Note) GetKey() string { return "" }
  function NewNote (line 41) | func NewNote() *Note {
  function render (line 307) | func render(input string) string {

FILE: field_select.go
  constant minHeight (line 21) | minHeight     = 1
  constant defaultHeight (line 22) | defaultHeight = 10
  type Select (line 31) | type Select struct
  function NewSelect (line 66) | func NewSelect[T comparable]() *Select[T] {
  method Value (line 86) | func (s *Select[T]) Value(value *T) *Select[T] {
  method Accessor (line 91) | func (s *Select[T]) Accessor(accessor Accessor[T]) *Select[T] {
  method selectValue (line 98) | func (s *Select[T]) selectValue(value T) {
  method Key (line 109) | func (s *Select[T]) Key(key string) *Select[T] {
  method Title (line 117) | func (s *Select[T]) Title(title string) *Select[T] {
  method TitleFunc (line 130) | func (s *Select[T]) TitleFunc(f func() string, bindings any) *Select[T] {
  method Filtering (line 137) | func (s *Select[T]) Filtering(filtering bool) *Select[T] {
  method Description (line 146) | func (s *Select[T]) Description(description string) *Select[T] {
  method DescriptionFunc (line 158) | func (s *Select[T]) DescriptionFunc(f func() string, bindings any) *Sele...
  method Options (line 176) | func (s *Select[T]) Options(options ...Option[T]) *Select[T] {
  method selectOption (line 191) | func (s *Select[T]) selectOption() {
  method OptionsFunc (line 229) | func (s *Select[T]) OptionsFunc(f func() []Option[T], bindings any) *Sel...
  method Inline (line 242) | func (s *Select[T]) Inline(v bool) *Select[T] {
  method Height (line 256) | func (s *Select[T]) Height(height int) *Select[T] {
  method Validate (line 263) | func (s *Select[T]) Validate(validate func(T) error) *Select[T] {
  method Error (line 269) | func (s *Select[T]) Error() error { return s.err }
  method Skip (line 272) | func (*Select[T]) Skip() bool { return false }
  method Zoom (line 275) | func (*Select[T]) Zoom() bool { return false }
  method Focus (line 278) | func (s *Select[T]) Focus() tea.Cmd {
  method Blur (line 284) | func (s *Select[T]) Blur() tea.Cmd {
  method Hovered (line 298) | func (s *Select[T]) Hovered() (T, bool) {
  method KeyBinds (line 307) | func (s *Select[T]) KeyBinds() []key.Binding {
  method Init (line 323) | func (s *Select[T]) Init() tea.Cmd {
  method Update (line 328) | func (s *Select[T]) Update(msg tea.Msg) (Model, tea.Cmd) {
  method updateValue (line 515) | func (s *Select[T]) updateValue() {
  method updateViewportSize (line 523) | func (s *Select[T]) updateViewportSize() {
  method activeStyles (line 547) | func (s *Select[T]) activeStyles() *FieldStyles {
  method titleView (line 558) | func (s *Select[T]) titleView() string {
  method descriptionView (line 577) | func (s *Select[T]) descriptionView() string {
  method optionsView (line 585) | func (s *Select[T]) optionsView() (string, int, int) {
  method cursorLineOffset (line 640) | func (s *Select[T]) cursorLineOffset() (offset int, height int) {
  function ensureVisible (line 657) | func ensureVisible(vp *viewport.Model, offset, height int) {
  method ensureCursorVisible (line 670) | func (s *Select[T]) ensureCursorVisible() {
  method renderOption (line 675) | func (s *Select[T]) renderOption(option Option[T], selected bool) string {
  method View (line 700) | func (s *Select[T]) View() string {
  method clearFilter (line 718) | func (s *Select[T]) clearFilter() {
  method setFiltering (line 725) | func (s *Select[T]) setFiltering(filtering bool) {
  method filterFunc (line 736) | func (s *Select[T]) filterFunc(option string) bool {
  method Run (line 742) | func (s *Select[T]) Run() error {
  method RunAccessible (line 747) | func (s *Select[T]) RunAccessible(w io.Writer, r io.Reader) error {
  method WithTheme (line 782) | func (s *Select[T]) WithTheme(theme Theme) Field {
  method WithKeyMap (line 801) | func (s *Select[T]) WithKeyMap(k *KeyMap) Field {
  method WithWidth (line 811) | func (s *Select[T]) WithWidth(width int) Field {
  method WithHeight (line 818) | func (s *Select[T]) WithHeight(height int) Field {
  method WithPosition (line 823) | func (s *Select[T]) WithPosition(p FieldPosition) Field {
  method GetKey (line 834) | func (s *Select[T]) GetKey() string { return s.key }
  method GetValue (line 837) | func (s *Select[T]) GetValue() any {
  method GetFiltering (line 842) | func (s *Select[T]) GetFiltering() bool {

FILE: field_text.go
  type Text (line 24) | type Text struct
    method Value (line 84) | func (t *Text) Value(value *string) *Text {
    method Accessor (line 89) | func (t *Text) Accessor(accessor Accessor[string]) *Text {
    method Key (line 96) | func (t *Text) Key(key string) *Text {
    method Title (line 104) | func (t *Text) Title(title string) *Text {
    method TitleFunc (line 117) | func (t *Text) TitleFunc(f func() string, bindings any) *Text {
    method Description (line 126) | func (t *Text) Description(description string) *Text {
    method DescriptionFunc (line 139) | func (t *Text) DescriptionFunc(f func() string, bindings any) *Text {
    method Lines (line 146) | func (t *Text) Lines(lines int) *Text {
    method CharLimit (line 152) | func (t *Text) CharLimit(charlimit int) *Text {
    method ShowLineNumbers (line 158) | func (t *Text) ShowLineNumbers(show bool) *Text {
    method Placeholder (line 166) | func (t *Text) Placeholder(str string) *Text {
    method PlaceholderFunc (line 178) | func (t *Text) PlaceholderFunc(f func() string, bindings any) *Text {
    method Validate (line 185) | func (t *Text) Validate(validate func(string) error) *Text {
    method ExternalEditor (line 191) | func (t *Text) ExternalEditor(enabled bool) *Text {
    method Editor (line 211) | func (t *Text) Editor(editor ...string) *Text {
    method EditorExtension (line 222) | func (t *Text) EditorExtension(extension string) *Text {
    method Error (line 228) | func (t *Text) Error() error { return t.err }
    method Skip (line 231) | func (*Text) Skip() bool { return false }
    method Zoom (line 234) | func (*Text) Zoom() bool { return false }
    method Focus (line 237) | func (t *Text) Focus() tea.Cmd {
    method Blur (line 243) | func (t *Text) Blur() tea.Cmd {
    method KeyBinds (line 252) | func (t *Text) KeyBinds() []key.Binding {
    method Init (line 260) | func (t *Text) Init() tea.Cmd {
    method Update (line 266) | func (t *Text) Update(msg tea.Msg) (Model, tea.Cmd) {
    method activeStyles (line 365) | func (t *Text) activeStyles() *FieldStyles {
    method View (line 377) | func (t *Text) View() string {
    method Run (line 414) | func (t *Text) Run() error {
    method RunAccessible (line 419) | func (t *Text) RunAccessible(w io.Writer, r io.Reader) error {
    method WithTheme (line 445) | func (t *Text) WithTheme(theme Theme) Field {
    method WithKeyMap (line 454) | func (t *Text) WithKeyMap(k *KeyMap) Field {
    method WithWidth (line 461) | func (t *Text) WithWidth(width int) Field {
    method WithHeight (line 468) | func (t *Text) WithHeight(height int) Field {
    method WithPosition (line 481) | func (t *Text) WithPosition(p FieldPosition) Field {
    method GetKey (line 489) | func (t *Text) GetKey() string { return t.key }
    method GetValue (line 492) | func (t *Text) GetValue() any {
  function NewText (line 56) | func NewText() *Text {
  constant defaultEditor (line 196) | defaultEditor = "nano"
  function getEditor (line 199) | func getEditor() (string, []string) {
  type updateValueMsg (line 257) | type updateValueMsg

FILE: form.go
  constant defaultWidth (line 20) | defaultWidth = 80
  function nextID (line 30) | func nextID() int {
  type FormState (line 41) | type FormState
  constant StateNormal (line 45) | StateNormal FormState = iota
  constant StateCompleted (line 48) | StateCompleted
  constant StateAborted (line 51) | StateAborted
  type Form (line 67) | type Form struct
    method WithAccessible (line 235) | func (f *Form) WithAccessible(accessible bool) *Form {
    method WithShowHelp (line 244) | func (f *Form) WithShowHelp(v bool) *Form {
    method WithShowErrors (line 256) | func (f *Form) WithShowErrors(v bool) *Form {
    method WithTheme (line 269) | func (f *Form) WithTheme(theme Theme) *Form {
    method WithKeyMap (line 284) | func (f *Form) WithKeyMap(keymap *KeyMap) *Form {
    method WithWidth (line 302) | func (f *Form) WithWidth(width int) *Form {
    method WithHeight (line 316) | func (f *Form) WithHeight(height int) *Form {
    method WithOutput (line 330) | func (f *Form) WithOutput(w io.Writer) *Form {
    method WithInput (line 338) | func (f *Form) WithInput(r io.Reader) *Form {
    method WithTimeout (line 345) | func (f *Form) WithTimeout(t time.Duration) *Form {
    method WithProgramOptions (line 351) | func (f *Form) WithProgramOptions(opts ...tea.ProgramOption) *Form {
    method WithViewHook (line 357) | func (f *Form) WithViewHook(hook compat.ViewHook) *Form {
    method WithLayout (line 365) | func (f *Form) WithLayout(layout Layout) *Form {
    method UpdateFieldPositions (line 371) | func (f *Form) UpdateFieldPositions() *Form {
    method Errors (line 432) | func (f *Form) Errors() []error {
    method Help (line 437) | func (f *Form) Help() help.Model {
    method KeyBinds (line 442) | func (f *Form) KeyBinds() []key.Binding {
    method Get (line 448) | func (f *Form) Get(key string) any {
    method GetString (line 453) | func (f *Form) GetString(key string) string {
    method GetInt (line 462) | func (f *Form) GetInt(key string) int {
    method GetBool (line 471) | func (f *Form) GetBool(key string) bool {
    method NextGroup (line 480) | func (f *Form) NextGroup() tea.Cmd {
    method PrevGroup (line 486) | func (f *Form) PrevGroup() tea.Cmd {
    method NextField (line 492) | func (f *Form) NextField() tea.Cmd {
    method PrevField (line 498) | func (f *Form) PrevField() tea.Cmd {
    method GetFocusedField (line 504) | func (f *Form) GetFocusedField() Field {
    method Init (line 509) | func (f *Form) Init() tea.Cmd {
    method Update (line 528) | func (f *Form) Update(msg tea.Msg) (Model, tea.Cmd) {
    method isGroupHidden (line 634) | func (f *Form) isGroupHidden(group *Group) bool {
    method getTheme (line 642) | func (f *Form) getTheme() *Styles {
    method styles (line 649) | func (f *Form) styles() FormStyles {
    method View (line 654) | func (f *Form) View() string {
    method Run (line 663) | func (f *Form) Run() error {
    method RunWithContext (line 668) | func (f *Form) RunWithContext(ctx context.Context) error {
    method run (line 687) | func (f *Form) run(ctx context.Context) error {
    method runAccessible (line 721) | func (f *Form) runAccessible(w io.Writer, r io.Reader) error {
  function NewForm (line 109) | func NewForm(groups ...*Group) *Form {
  type Field (line 143) | type Field interface
  type FieldPosition (line 193) | type FieldPosition struct
    method IsFirst (line 204) | func (p FieldPosition) IsFirst() bool {
    method IsLast (line 209) | func (p FieldPosition) IsLast() bool {
  type nextGroupMsg (line 214) | type nextGroupMsg struct
  type prevGroupMsg (line 217) | type prevGroupMsg struct
  function nextGroup (line 220) | func nextGroup() tea.Msg {
  function prevGroup (line 225) | func prevGroup() tea.Msg {

FILE: group.go
  type Group (line 19) | type Group struct
    method Title (line 71) | func (g *Group) Title(title string) *Group {
    method Description (line 77) | func (g *Group) Description(description string) *Group {
    method WithShowHelp (line 83) | func (g *Group) WithShowHelp(show bool) *Group {
    method WithShowErrors (line 89) | func (g *Group) WithShowErrors(show bool) *Group {
    method WithTheme (line 95) | func (g *Group) WithTheme(t Theme) *Group {
    method WithKeyMap (line 109) | func (g *Group) WithKeyMap(k *KeyMap) *Group {
    method WithWidth (line 119) | func (g *Group) WithWidth(width int) *Group {
    method WithHeight (line 131) | func (g *Group) WithHeight(height int) *Group {
    method WithHide (line 146) | func (g *Group) WithHide(hide bool) *Group {
    method WithHideFunc (line 152) | func (g *Group) WithHideFunc(hideFunc func() bool) *Group {
    method Errors (line 158) | func (g *Group) Errors() []error {
    method Init (line 199) | func (g *Group) Init() tea.Cmd {
    method nextField (line 222) | func (g *Group) nextField() []tea.Cmd {
    method prevField (line 239) | func (g *Group) prevField() []tea.Cmd {
    method Update (line 256) | func (g *Group) Update(msg tea.Msg) (Model, tea.Cmd) {
    method getTheme (line 294) | func (g *Group) getTheme() *Styles {
    method styles (line 301) | func (g *Group) styles() GroupStyles { return g.getTheme().Group }
    method getContent (line 303) | func (g *Group) getContent() (int, string) {
    method buildView (line 329) | func (g *Group) buildView() {
    method Header (line 336) | func (g *Group) Header() string {
    method titleFooterHeight (line 349) | func (g *Group) titleFooterHeight() int {
    method rawHeight (line 361) | func (g *Group) rawHeight() int {
    method View (line 366) | func (g *Group) View() string {
    method Content (line 387) | func (g *Group) Content() string {
    method Footer (line 393) | func (g *Group) Footer() string {
  function NewGroup (line 48) | func NewGroup(fields ...Field) *Group {
  type updateFieldMsg (line 174) | type updateFieldMsg struct
  type nextFieldMsg (line 180) | type nextFieldMsg struct
  type prevFieldMsg (line 186) | type prevFieldMsg struct
  function NextField (line 189) | func NextField() tea.Msg {
  function PrevField (line 194) | func PrevField() tea.Msg {

FILE: huh_test.go
  constant text (line 20) | text = "Huh"
  function TestForm (line 28) | func TestForm(t *testing.T) {
  function TestInput (line 280) | func TestInput(t *testing.T) {
  function TestPasteNotDuplicated (line 311) | func TestPasteNotDuplicated(t *testing.T) {
  function TestInlineInput (line 323) | func TestInlineInput(t *testing.T) {
  function TestText (line 364) | func TestText(t *testing.T) {
  function TestTextExternalEditorHidden (line 388) | func TestTextExternalEditorHidden(t *testing.T) {
  function TestConfirm (line 412) | func TestConfirm(t *testing.T) {
  function TestSelect (line 458) | func TestSelect(t *testing.T) {
  function doAllUpdates (line 521) | func doAllUpdates(f *Form, cmd tea.Cmd) {
  function TestSelectDynamic (line 539) | func TestSelectDynamic(t *testing.T) {
  function TestMultiSelect (line 620) | func TestMultiSelect(t *testing.T) {
  function TestMultiSelectFiltering (line 701) | func TestMultiSelectFiltering(t *testing.T) {
  function TestSelectPageNavigation (line 744) | func TestSelectPageNavigation(t *testing.T) {
  function TestFile (line 835) | func TestFile(t *testing.T) {
  function TestHideGroup (line 853) | func TestHideGroup(t *testing.T) {
  function TestHideGroupLastAndFirstGroupsNotHidden (line 898) | func TestHideGroupLastAndFirstGroupsNotHidden(t *testing.T) {
  function TestPrevGroup (line 935) | func TestPrevGroup(t *testing.T) {
  function TestNote (line 954) | func TestNote(t *testing.T) {
  function TestDynamicHelp (line 991) | func TestDynamicHelp(t *testing.T) {
  function TestSkip (line 1014) | func TestSkip(t *testing.T) {
  function TestTimeout (line 1061) | func TestTimeout(t *testing.T) {
  function TestAbort (line 1072) | func TestAbort(t *testing.T) {
  constant title (line 1090) | title       = "A Title"
  constant description (line 1091) | description = "A Description"
  function TestNoTitleOrDescription (line 1150) | func TestNoTitleOrDescription(t *testing.T) {
  function TestTitleRowRender (line 1164) | func TestTitleRowRender(t *testing.T) {
  function TestDescriptionRowRender (line 1176) | func TestDescriptionRowRender(t *testing.T) {
  function TestGetFocusedField (line 1188) | func TestGetFocusedField(t *testing.T) {
  function formProgram (line 1207) | func formProgram() *Form {
  function batchUpdate (line 1214) | func batchUpdate(m Model, cmd tea.Cmd) Model {
  function codeKeypress (line 1228) | func codeKeypress(r rune) tea.KeyPressMsg {
  function keypress (line 1234) | func keypress(r rune) tea.KeyPressMsg {
  function typeText (line 1242) | func typeText[T Model](m T, s string) T {
  function TestAccessibleForm (line 1250) | func TestAccessibleForm(t *testing.T) {
  function TestAccessibleFields (line 1271) | func TestAccessibleFields(t *testing.T) {
  function TestInputPasswordAccessible (line 1533) | func TestInputPasswordAccessible(t *testing.T) {
  function requireEqual (line 1578) | func requireEqual[T comparable](tb testing.TB, a, b T) {
  function requireContains (line 1585) | func requireContains(tb testing.TB, s, subtr string) {
  function viewModel (line 1592) | func viewModel(m Model) string { return ansi.Strip(m.View()) }

FILE: internal/accessibility/accessibility.go
  function atoi (line 17) | func atoi(s string) (int, error) {
  function PromptInt (line 29) | func PromptInt(
  function parseBool (line 63) | func parseBool(s string) (bool, error) {
  function PromptBool (line 83) | func PromptBool(
  function PromptPassword (line 108) | func PromptPassword(
  function PromptString (line 131) | func PromptString(
  function ptrToStr (line 167) | func ptrToStr[T any](t *T, fn func(t T) string) string {
  function boolToStr (line 174) | func boolToStr(b bool) string {

FILE: internal/compat/model.go
  type Model (line 7) | type Model interface
  type ViewModel (line 17) | type ViewModel struct
    method Update (line 23) | func (w ViewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    method View (line 32) | func (w ViewModel) View() tea.View {

FILE: internal/selector/selector.go
  type Selector (line 5) | type Selector struct
  function NewSelector (line 11) | func NewSelector[T any](items []T) *Selector[T] {
  method Append (line 18) | func (s *Selector[T]) Append(item T) {
  method Next (line 23) | func (s *Selector[T]) Next() {
  method Prev (line 30) | func (s *Selector[T]) Prev() {
  method OnFirst (line 37) | func (s *Selector[T]) OnFirst() bool {
  method OnLast (line 42) | func (s *Selector[T]) OnLast() bool {
  method Selected (line 47) | func (s *Selector[T]) Selected() T {
  method Index (line 52) | func (s *Selector[T]) Index() int {
  method Total (line 57) | func (s *Selector[T]) Total() int {
  method SetIndex (line 62) | func (s *Selector[T]) SetIndex(i int) {
  method Get (line 70) | func (s *Selector[T]) Get(i int) T {
  method Set (line 75) | func (s *Selector[T]) Set(i int, item T) {
  method Range (line 81) | func (s *Selector[T]) Range(f func(i int, item T) bool) {
  method ReverseRange (line 91) | func (s *Selector[T]) ReverseRange(f func(i int, item T) bool) {

FILE: keymap.go
  type KeyMap (line 6) | type KeyMap struct
  type InputKeyMap (line 19) | type InputKeyMap struct
  type TextKeyMap (line 27) | type TextKeyMap struct
  type SelectKeyMap (line 36) | type SelectKeyMap struct
  type MultiSelectKeyMap (line 54) | type MultiSelectKeyMap struct
  type FilePickerKeyMap (line 73) | type FilePickerKeyMap struct
  type NoteKeyMap (line 90) | type NoteKeyMap struct
  type ConfirmKeyMap (line 97) | type ConfirmKeyMap struct
  function NewDefaultKeyMap (line 107) | func NewDefaultKeyMap() *KeyMap {

FILE: layout.go
  type Layout (line 10) | type Layout interface
  function LayoutColumns (line 22) | func LayoutColumns(columns int) Layout {
  function LayoutGrid (line 27) | func LayoutGrid(rows int, columns int) Layout {
  type layoutDefault (line 31) | type layoutDefault struct
    method View (line 33) | func (l *layoutDefault) View(f *Form) string {
    method GroupWidth (line 37) | func (l *layoutDefault) GroupWidth(_ *Form, _ *Group, w int) int {
  type layoutColumns (line 41) | type layoutColumns struct
    method visibleGroups (line 45) | func (l *layoutColumns) visibleGroups(f *Form) []*Group {
    method View (line 67) | func (l *layoutColumns) View(f *Form) string {
    method GroupWidth (line 88) | func (l *layoutColumns) GroupWidth(_ *Form, _ *Group, w int) int {
  type layoutStack (line 92) | type layoutStack struct
    method View (line 94) | func (l *layoutStack) View(f *Form) string {
    method GroupWidth (line 107) | func (l *layoutStack) GroupWidth(_ *Form, _ *Group, w int) int {
  type layoutGrid (line 111) | type layoutGrid struct
    method visibleGroups (line 115) | func (l *layoutGrid) visibleGroups(f *Form) [][]*Group {
    method View (line 148) | func (l *layoutGrid) View(f *Form) string {
    method GroupWidth (line 167) | func (l *layoutGrid) GroupWidth(_ *Form, _ *Group, w int) int {

FILE: option.go
  type Option (line 6) | type Option struct
  function NewOptions (line 13) | func NewOptions[T comparable](values ...T) []Option[T] {
  function NewOption (line 25) | func NewOption[T comparable](key string, value T) Option[T] {
  method Selected (line 30) | func (o Option[T]) Selected(selected bool) Option[T] {
  method String (line 36) | func (o Option[T]) String() string {

FILE: run.go
  function Run (line 4) | func Run(field Field) error {

FILE: spinner/spinner.go
  type Spinner (line 29) | type Spinner struct
    method Type (line 95) | func (s *Spinner) Type(t Type) *Spinner {
    method Title (line 101) | func (s *Spinner) Title(title string) *Spinner {
    method WithOutput (line 108) | func (s *Spinner) WithOutput(w io.Writer) *Spinner {
    method WithInput (line 116) | func (s *Spinner) WithInput(r io.Reader) *Spinner {
    method WithViewHook (line 123) | func (s *Spinner) WithViewHook(hook compat.ViewHook) *Spinner {
    method Action (line 129) | func (s *Spinner) Action(action func()) *Spinner {
    method ActionWithErr (line 141) | func (s *Spinner) ActionWithErr(action func(context.Context) error) *S...
    method Context (line 147) | func (s *Spinner) Context(ctx context.Context) *Spinner {
    method WithAccessible (line 153) | func (s *Spinner) WithAccessible(accessible bool) *Spinner {
    method WithTheme (line 170) | func (s *Spinner) WithTheme(theme Theme) *Spinner {
    method Init (line 180) | func (s *Spinner) Init() tea.Cmd {
    method Update (line 195) | func (s *Spinner) Update(msg tea.Msg) (Model, tea.Cmd) {
    method View (line 215) | func (s *Spinner) View() string {
    method Run (line 226) | func (s *Spinner) Run() error {
    method runAccessible (line 262) | func (s *Spinner) runAccessible(in io.Reader, out io.Writer) error {
  type Styles (line 45) | type Styles struct
  type Theme (line 50) | type Theme interface
  type ThemeFunc (line 55) | type ThemeFunc
    method Theme (line 58) | func (f ThemeFunc) Theme(isDark bool) *Styles {
  function ThemeDefault (line 63) | func ThemeDefault(isDark bool) *Styles {
  type Type (line 76) | type Type
  function New (line 159) | func New() *Spinner {
  type doneMsg (line 299) | type doneMsg struct

FILE: spinner/spinner_test.go
  function TestNewSpinner (line 17) | func TestNewSpinner(t *testing.T) {
  function TestSpinnerType (line 27) | func TestSpinnerType(t *testing.T) {
  function TestSpinnerDifferentTypes (line 34) | func TestSpinnerDifferentTypes(t *testing.T) {
  function TestSpinnerView (line 41) | func TestSpinnerView(t *testing.T) {
  function TestSpinnerContextCancellation (line 50) | func TestSpinnerContextCancellation(t *testing.T) {
  function TestSpinnerContextCancellationWhileRunning (line 59) | func TestSpinnerContextCancellationWhileRunning(t *testing.T) {
  function TestSpinnerStyleMethods (line 70) | func TestSpinnerStyleMethods(t *testing.T) {
  function TestSpinnerInit (line 87) | func TestSpinnerInit(t *testing.T) {
  function TestSpinnerUpdate (line 96) | func TestSpinnerUpdate(t *testing.T) {
  function TestSpinnerSimple (line 119) | func TestSpinnerSimple(t *testing.T) {
  function TestSpinnerWithContextAndAction (line 125) | func TestSpinnerWithContextAndAction(t *testing.T) {
  function TestSpinnerWithActionError (line 132) | func TestSpinnerWithActionError(t *testing.T) {
  function exercise (line 139) | func exercise(t *testing.T, factory func() *Spinner, checker func(tb tes...
  function requireNoError (line 159) | func requireNoError(tb testing.TB, err error) {
  function requireErrorIs (line 166) | func requireErrorIs(target error) func(tb testing.TB, err error) {
  function requireContextCanceled (line 175) | func requireContextCanceled(tb testing.TB, err error) {
  type nilReader (line 185) | type nilReader struct
    method Read (line 188) | func (nilReader) Read([]byte) (int, error) { return 0, nil }

FILE: theme.go
  type Theme (line 10) | type Theme interface
  type ThemeFunc (line 15) | type ThemeFunc
    method Theme (line 18) | func (f ThemeFunc) Theme(isDark bool) *Styles {
  type Styles (line 24) | type Styles struct
  type FormStyles (line 34) | type FormStyles struct
  type GroupStyles (line 39) | type GroupStyles struct
  type FieldStyles (line 46) | type FieldStyles struct
  type TextInputStyles (line 84) | type TextInputStyles struct
  constant buttonPaddingHorizontal (line 93) | buttonPaddingHorizontal = 2
  constant buttonPaddingVertical (line 94) | buttonPaddingVertical   = 0
  function ThemeBase (line 99) | func ThemeBase(bool) *Styles {
  function ThemeCharm (line 139) | func ThemeCharm(isDark bool) *Styles {
  function ThemeDracula (line 189) | func ThemeDracula(isDark bool) *Styles {
  function ThemeBase16 (line 240) | func ThemeBase16(isDark bool) *Styles {
  function ThemeCatppuccin (line 285) | func ThemeCatppuccin(isDark bool) *Styles {

FILE: validate.go
  function ValidateNotEmpty (line 9) | func ValidateNotEmpty() func(s string) error {
  function ValidateMinLength (line 19) | func ValidateMinLength(v int) func(s string) error {
  function ValidateMaxLength (line 29) | func ValidateMaxLength(v int) func(s string) error {
  function ValidateLength (line 39) | func ValidateLength(minl, maxl int) func(s string) error {
  function ValidateOneOf (line 49) | func ValidateOneOf(options ...string) func(string) error {

FILE: wrap.go
  function wrap (line 5) | func wrap(s string, limit int) string {

FILE: zz_resize_width_test.go
  function TestSelectWithWidthUpdatesViewportWidth (line 5) | func TestSelectWithWidthUpdatesViewportWidth(t *testing.T) {
  function TestMultiSelectWithWidthUpdatesViewportWidth (line 24) | func TestMultiSelectWithWidthUpdatesViewportWidth(t *testing.T) {
Condensed preview — 108 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (380K chars).
[
  {
    "path": ".gitattributes",
    "chars": 84,
    "preview": "*.gif filter=lfs diff=lfs merge=lfs -text\n*.png filter=lfs diff=lfs merge=lfs -text\n"
  },
  {
    "path": ".github/CODEOWNERS",
    "chars": 26,
    "preview": "* @charmbracelet/everyone\n"
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 2009,
    "preview": "version: 2\n\nupdates:\n  - package-ecosystem: \"gomod\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n      day:"
  },
  {
    "path": ".github/workflows/build.yml",
    "chars": 445,
    "preview": "name: build\non: [push, pull_request]\njobs:\n  build:\n    uses: charmbracelet/meta/.github/workflows/build.yml@main\n\n  bui"
  },
  {
    "path": ".github/workflows/dependabot-sync.yml",
    "chars": 419,
    "preview": "name: dependabot-sync\non:\n  schedule:\n    - cron: \"0 0 * * 0\" # every Sunday at midnight\n  workflow_dispatch: # allows m"
  },
  {
    "path": ".github/workflows/lint-sync.yml",
    "chars": 271,
    "preview": "name: lint-sync\non:\n  schedule:\n    # every Sunday at midnight\n    - cron: \"0 0 * * 0\"\n  workflow_dispatch: # allows man"
  },
  {
    "path": ".github/workflows/lint.yml",
    "chars": 179,
    "preview": "name: lint\non:\n  push:\n  pull_request:\n\njobs:\n  lint:\n    uses: charmbracelet/meta/.github/workflows/lint.yml@main\n    w"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 1106,
    "preview": "name: goreleaser\n\non:\n  push:\n    tags:\n      - v*.*.*\n\nconcurrency:\n  group: goreleaser\n  cancel-in-progress: true\n\njob"
  },
  {
    "path": ".gitignore",
    "chars": 501,
    "preview": "# If you prefer the allow list template instead of the deny list, see community template:\n# https://github.com/github/gi"
  },
  {
    "path": ".golangci.yml",
    "chars": 764,
    "preview": "version: \"2\"\nrun:\n  tests: false\nlinters:\n  enable:\n    - bodyclose\n    - exhaustive\n    - goconst\n    - godot\n    - gom"
  },
  {
    "path": ".goreleaser.yml",
    "chars": 159,
    "preview": "includes:\n  - from_url:\n      url: charmbracelet/meta/main/goreleaser-lib.yaml\n\n# yaml-language-server: $schema=https://"
  },
  {
    "path": "LICENSE",
    "chars": 1067,
    "preview": "MIT License\n\nCopyright (c) 2023–2026 Charm\n\nPermission is hereby granted, free of charge, to any person obtaining a copy"
  },
  {
    "path": "Makefile",
    "chars": 215,
    "preview": ".PHONY: spinner\n\n$(V).SILENT:\ntest:\n\tgo test ./...\n\nspinner:\n\tcd spinner/examples/loading && go run .\n\nburger:\n\tcd examp"
  },
  {
    "path": "README.md",
    "chars": 15167,
    "preview": "# Huh?\n\n<p>\n  <img src=\"https://stuff.charm.sh/huh/glenn.png\" width=\"400\" />\n  <br><br>\n  <a href=\"https://github.com/ch"
  },
  {
    "path": "UPGRADE_GUIDE_V2.md",
    "chars": 8135,
    "preview": "# Huh v2 Upgrade Guide\n\nThis guide will help you migrate from Huh v1 to v2. Most changes are straightforward, and many a"
  },
  {
    "path": "accessor.go",
    "chars": 885,
    "preview": "package huh\n\n// Accessor give read/write access to field values.\ntype Accessor[T any] interface {\n\tGet() T\n\tSet(value T)"
  },
  {
    "path": "eval.go",
    "chars": 1459,
    "preview": "package huh\n\nimport (\n\t\"time\"\n\n\t\"github.com/mitchellh/hashstructure/v2\"\n)\n\n// Eval is an evaluatable value, it stores a "
  },
  {
    "path": "examples/.gitignore",
    "chars": 5,
    "preview": ".ssh\n"
  },
  {
    "path": "examples/accessibility/accessible.tape",
    "chars": 328,
    "preview": "Output accessible.gif\n\nSet Height 600\nSet Width 1000\n\nHide\n  Type \"go build -o accessible .\" Enter\n  Type \"export ACCESS"
  },
  {
    "path": "examples/accessibility/main.go",
    "chars": 441,
    "preview": "package main\n\nimport (\n\t\"log\"\n\n\t\"charm.land/huh/v2\"\n)\n\nfunc main() {\n\tform := huh.NewForm(\n\t\thuh.NewGroup(\n\t\t\thuh.NewSel"
  },
  {
    "path": "examples/accessibility-secure-input/main.go",
    "chars": 984,
    "preview": "package main\n\nimport (\n\t\"errors\"\n\t\"log\"\n\n\t\"charm.land/huh/v2\"\n)\n\nfunc validate(s string) error {\n\tif s == \"\" {\n\t\treturn "
  },
  {
    "path": "examples/bubbletea/demo.tape",
    "chars": 232,
    "preview": "Set Height 775\nSet Padding 60\nSet Width 1200\nSet FontSize 20\n\nHide\n  Type \"clear && go run .\"\n  Enter\n  Sleep 1s\nShow\nSl"
  },
  {
    "path": "examples/bubbletea/main.go",
    "chars": 7126,
    "preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"image/color\"\n\t\"os\"\n\t\"strings\"\n\n\ttea \"charm.land/bubbletea/v2\"\n\t\"charm.land/huh/v2\"\n\t\"cha"
  },
  {
    "path": "examples/bubbletea-options/main.go",
    "chars": 415,
    "preview": "package main\n\nimport (\n\t\"fmt\"\n\n\ttea \"charm.land/bubbletea/v2\"\n\t\"charm.land/huh/v2\"\n)\n\nfunc main() {\n\tvar name string\n\tfo"
  },
  {
    "path": "examples/burger/demo.tape",
    "chars": 580,
    "preview": "Output burger.gif\n\nSet Height 700\nSet Width 1000\n\nHide\nType \"go build -o burger .\" Enter\nCtrl+L\nSleep 1s\n\nType \"clear &&"
  },
  {
    "path": "examples/burger/main.go",
    "chars": 4597,
    "preview": "package main\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"charm.land/huh/v2\"\n\t\"charm.land/huh/v2/sp"
  },
  {
    "path": "examples/conditional/main.go",
    "chars": 1883,
    "preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"charm.land/huh/v2\"\n)\n\ntype consumable int\n\nconst (\n\tfruits consumable = iota\n\tveg"
  },
  {
    "path": "examples/dynamic/demo.tape",
    "chars": 357,
    "preview": "Output dynamic.gif\n\nSet Shell \"bash\"\nSet FontSize 28\nSet Width 1000\nSet Height 700\n\nHide\n  Type \"clear && go build -o dy"
  },
  {
    "path": "examples/dynamic/dynamic-all/main.go",
    "chars": 1579,
    "preview": "package main\n\nimport (\n\t\"log\"\n\t\"strconv\"\n\n\t\"charm.land/huh/v2\"\n)\n\nfunc main() {\n\tvar value string = \"Dynamic\"\n\n\tf := huh"
  },
  {
    "path": "examples/dynamic/dynamic-bubbletea/main.go",
    "chars": 7332,
    "preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"image/color\"\n\t\"os\"\n\t\"strings\"\n\n\ttea \"charm.land/bubbletea/v2\"\n\t\"charm.land/huh/v2\"\n\t\"cha"
  },
  {
    "path": "examples/dynamic/dynamic-count/main.go",
    "chars": 1229,
    "preview": "package main\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"log\"\n\t\"strconv\"\n\n\t\"charm.land/huh/v2\"\n)\n\nfunc main() {\n\tvar value string\n\tdefa"
  },
  {
    "path": "examples/dynamic/dynamic-country/main.go",
    "chars": 2540,
    "preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"charm.land/huh/v2\"\n\t\"charm.land/log/v2\"\n)\n\nfunc main() {\n\tlog.SetReportTimestam"
  },
  {
    "path": "examples/dynamic/dynamic-increment/main.go",
    "chars": 477,
    "preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"charm.land/huh/v2\"\n)\n\nfunc main() {\n\tcount := 0\n\tgo func() {\n\t\tfor {\n\t\t\tcount++"
  },
  {
    "path": "examples/dynamic/dynamic-markdown/main.go",
    "chars": 445,
    "preview": "package main\n\nimport (\n\t\"log\"\n\n\t\"charm.land/glamour/v2\"\n\t\"charm.land/huh/v2\"\n)\n\nfunc main() {\n\tvar md string\n\terr := huh"
  },
  {
    "path": "examples/dynamic/dynamic-name/main.go",
    "chars": 1233,
    "preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\n\t\"charm.land/huh/v2\"\n)\n\nfunc main() {\n\tvar name string\n\n\terr := huh.NewForm(\n\t\thuh"
  },
  {
    "path": "examples/dynamic/dynamic-suggestions/main.go",
    "chars": 1084,
    "preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\n\t\"charm.land/huh/v2\"\n\t\"charm.land/huh/v2/spinner\"\n)\n\nfunc main() {\n\tvar org string"
  },
  {
    "path": "examples/filepicker/artichoke.hs",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "examples/filepicker/demo.tape",
    "chars": 317,
    "preview": "Set Shell bash\n\nSet Width 800\nSet Height 725\n\nHide\n  Type \"clear && go build -o file\"\n  Enter\nShow\n\nSleep .5s\nType \"./fi"
  },
  {
    "path": "examples/filepicker/main.go",
    "chars": 599,
    "preview": "package main\n\nimport (\n\t\"charm.land/huh/v2\"\n)\n\nfunc main() {\n\tvar file string\n\n\thuh.NewForm(\n\t\thuh.NewGroup(\n\t\t\thuh.NewI"
  },
  {
    "path": "examples/filepicker-picking/main.go",
    "chars": 323,
    "preview": "package main\n\nimport (\n\t\"fmt\"\n\n\t\"charm.land/huh/v2\"\n)\n\nfunc main() {\n\tvar file string\n\thuh.NewForm(\n\t\thuh.NewGroup(\n\t\t\th"
  },
  {
    "path": "examples/gh/create.go",
    "chars": 2216,
    "preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\n\t\"charm.land/huh/v2\"\n\t\"charm.land/huh/v2/spinner\"\n\t\"charm.land/lipgloss/v2\"\n"
  },
  {
    "path": "examples/git/main.go",
    "chars": 1113,
    "preview": "package main\n\nimport (\n\t\"charm.land/huh/v2\"\n)\n\n// types is the possible commit types specified by the conventional commi"
  },
  {
    "path": "examples/go.mod",
    "chars": 2446,
    "preview": "module examples\n\ngo 1.25.8\n\nreplace charm.land/huh/v2 => ../\n\nrequire (\n\tcharm.land/bubbles/v2 v2.0.0\n\tcharm.land/bubble"
  },
  {
    "path": "examples/go.sum",
    "chars": 10746,
    "preview": "charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s=\ncharm.land/bubbles/v2 v2.0.0/go.mod h1:rCHo"
  },
  {
    "path": "examples/gum/main.go",
    "chars": 484,
    "preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"charm.land/huh/v2\"\n)\n\nfunc main() {\n\tif len(os.Args) < 2 {\n\t\tfmt.Println(\"gum <in"
  },
  {
    "path": "examples/help/main.go",
    "chars": 233,
    "preview": "package main\n\nimport \"charm.land/huh/v2\"\n\nfunc main() {\n\tf := huh.NewForm(\n\t\thuh.NewGroup(\n\t\t\thuh.NewInput().Title(\"Dyna"
  },
  {
    "path": "examples/hide/hide.tape",
    "chars": 265,
    "preview": "Output hide.gif\n\nSet Width 700\nSet Padding 40\nSet Height 350\nSet FontSize 28\n\nHide\n  Type \"go build .\"\n  Sleep 500ms\n  E"
  },
  {
    "path": "examples/hide/main.go",
    "chars": 750,
    "preview": "package main\n\nimport (\n\t\"fmt\"\n\n\t\"charm.land/huh/v2\"\n)\n\nfunc main() {\n\tvar isAllergic bool\n\tvar allergies string\n\n\thuh.Ne"
  },
  {
    "path": "examples/layout/columns/main.go",
    "chars": 537,
    "preview": "package main\n\nimport \"charm.land/huh/v2\"\n\nfunc main() {\n\tform := huh.NewForm(\n\t\thuh.NewGroup(\n\t\t\thuh.NewInput().Title(\"F"
  },
  {
    "path": "examples/layout/default/main.go",
    "chars": 504,
    "preview": "package main\n\nimport \"charm.land/huh/v2\"\n\nfunc main() {\n\tform := huh.NewForm(\n\t\thuh.NewGroup(\n\t\t\thuh.NewInput().Title(\"F"
  },
  {
    "path": "examples/layout/grid/main.go",
    "chars": 671,
    "preview": "package main\n\nimport \"charm.land/huh/v2\"\n\nfunc main() {\n\tform := huh.NewForm(\n\t\thuh.NewGroup(\n\t\t\thuh.NewInput().Title(\"F"
  },
  {
    "path": "examples/layout/stack/main.go",
    "chars": 532,
    "preview": "package main\n\nimport \"charm.land/huh/v2\"\n\nfunc main() {\n\tform := huh.NewForm(\n\t\thuh.NewGroup(\n\t\t\thuh.NewInput().Title(\"F"
  },
  {
    "path": "examples/multiple-groups/main.go",
    "chars": 1904,
    "preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"charm.land/huh/v2\"\n)\n\nfunc main() {\n\tf := huh.NewForm(\n\t\thuh.NewGroup(\n\t\t\thuh.New"
  },
  {
    "path": "examples/readme/confirm/confirm.tape",
    "chars": 295,
    "preview": "Output confirm.gif\n\nSet Width 1100\nSet Padding 40\nSet Height 375\nSet FontSize 36\n\nHide\n  Type \"go build .\"\n  Sleep 500ms"
  },
  {
    "path": "examples/readme/confirm/main.go",
    "chars": 273,
    "preview": "package main\n\nimport (\n\t\"charm.land/huh/v2\"\n)\n\nfunc main() {\n\tvar happy bool\n\n\tconfirm := huh.NewConfirm().\n\t\tTitle(\"Are"
  },
  {
    "path": "examples/readme/input/input.tape",
    "chars": 272,
    "preview": "Output input.gif\n\nSet Width 1000\nSet Padding 30\nSet Height 275\nSet FontSize 38\n\nHide\n  Type \"go build .\"\n  Sleep 500ms\n "
  },
  {
    "path": "examples/readme/input/main.go",
    "chars": 723,
    "preview": "package main\n\nimport (\n\t\"fmt\"\n\n\t\"charm.land/huh/v2\"\n)\n\nfunc isFood(_ string) error {\n\treturn nil\n}\n\nfunc main() {\n\tvar l"
  },
  {
    "path": "examples/readme/input/suggestions.tape",
    "chars": 311,
    "preview": "Output suggestions.gif\n\nSet Width 1000\nSet Padding 30\nSet Height 275\nSet FontSize 38\n\nHide\n  Type \"go build .\"\n  Sleep 5"
  },
  {
    "path": "examples/readme/main/main.go",
    "chars": 3035,
    "preview": "package main\n\nimport (\n\t\"log\"\n\n\t\"charm.land/huh/v2\"\n)\n\n// TODO: ensure input is not plagiarized.\nfunc checkForPlagiarism"
  },
  {
    "path": "examples/readme/multiselect/main.go",
    "chars": 554,
    "preview": "package main\n\nimport \"charm.land/huh/v2\"\n\nfunc main() {\n\tvar toppings []string\n\ts := huh.NewMultiSelect[string]().\n\t\tOpt"
  },
  {
    "path": "examples/readme/multiselect/multiselect.tape",
    "chars": 361,
    "preview": "Output multiselect.gif\n\nSet Width 1150\nSet Padding 40\nSet Height 480\nSet FontSize 28\n\nHide\n  Type \"go build .\"\n  Sleep 5"
  },
  {
    "path": "examples/readme/note/main.go",
    "chars": 242,
    "preview": "package main\n\nimport \"charm.land/huh/v2\"\n\nfunc main() {\n\tnote := huh.NewNote().Description(\n\t\t\"# Heading\\n\" + \"This is _"
  },
  {
    "path": "examples/readme/select/main.go",
    "chars": 352,
    "preview": "package main\n\nimport \"charm.land/huh/v2\"\n\nfunc main() {\n\tvar country string\n\ts := huh.NewSelect[string]().\n\t\tTitle(\"Pick"
  },
  {
    "path": "examples/readme/select/scroll/scroll.go",
    "chars": 903,
    "preview": "package main\n\nimport \"charm.land/huh/v2\"\n\ntype Pokemon struct {\n\tid   int\n\tname string\n}\n\nvar pokemons = []Pokemon{\n\t{1,"
  },
  {
    "path": "examples/readme/select/scroll/scroll.tape",
    "chars": 270,
    "preview": "Output scroll.gif\n\nSet Width 800\nSet Padding 40\nSet Height 375\nSet FontSize 28\n\nHide\n  Type \"go build scroll.go\"\n  Sleep"
  },
  {
    "path": "examples/readme/select/select.tape",
    "chars": 340,
    "preview": "Output select.gif\n\nSet Width 1100\nSet Padding 40\nSet Height 375\nSet FontSize 28\n\nHide\n  Type \"go build .\"\n  Sleep 500ms\n"
  },
  {
    "path": "examples/readme/text/main.go",
    "chars": 401,
    "preview": "package main\n\nimport \"charm.land/huh/v2\"\n\n// TODO: ensure input is not plagiarized.\nfunc checkForPlagiarism(s string) er"
  },
  {
    "path": "examples/readme/text/text.tape",
    "chars": 491,
    "preview": "Output text.gif\n\nSet Width 1000\nSet Padding 40\nSet Height 450\nSet FontSize 28\n\nHide\n  Type \"go build .\" Enter\n  Ctrl+L\n "
  },
  {
    "path": "examples/scroll/main.go",
    "chars": 476,
    "preview": "package main\n\nimport \"charm.land/huh/v2\"\n\nfunc main() {\n\tform := huh.NewForm(\n\t\thuh.NewGroup(\n\t\t\thuh.NewInput().Title(\"F"
  },
  {
    "path": "examples/skip/main.go",
    "chars": 870,
    "preview": "package main\n\nimport (\n\t\"charm.land/huh/v2\"\n)\n\nfunc main() {\n\tf := huh.NewForm(\n\t\thuh.NewGroup(\n\t\t\thuh.NewNote().\n\t\t\t\tTi"
  },
  {
    "path": "examples/spinner/accessible/main.go",
    "chars": 298,
    "preview": "package main\n\nimport (\n\t\"context\"\n\t\"log\"\n\t\"time\"\n\n\t\"charm.land/huh/v2/spinner\"\n)\n\nfunc main() {\n\tctx, cancel := context."
  },
  {
    "path": "examples/spinner/context/main.go",
    "chars": 304,
    "preview": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"charm.land/huh/v2/spinner\"\n)\n\nfunc main() {\n\taction := func() { time"
  },
  {
    "path": "examples/spinner/context-and-action/main.go",
    "chars": 402,
    "preview": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"math/rand\"\n\t\"time\"\n\n\t\"charm.land/huh/v2/spinner\"\n)\n\nfunc main() {\n\tctx"
  },
  {
    "path": "examples/spinner/context-and-action-and-error/main.go",
    "chars": 423,
    "preview": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"time\"\n\n\t\"charm.land/huh/v2/spinner\"\n)\n\nfunc main() {\n\tctx, cancel := c"
  },
  {
    "path": "examples/spinner/loading/demo.tape",
    "chars": 173,
    "preview": "Output spinner.gif\n\nSet FontSize 32\nSet Height 225\nSet Width 800\n\nHide\nType \"go build -o spinner .\" Enter\nCtrl+L\nSleep 1"
  },
  {
    "path": "examples/spinner/loading/main.go",
    "chars": 474,
    "preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\ttea \"charm.land/bubbletea/v2\"\n\t\"charm.land/huh/v2/spinner\"\n)\n\nfunc main() {\n\tact"
  },
  {
    "path": "examples/spinner/static/main.go",
    "chars": 164,
    "preview": "package main\n\nimport (\n\t\"fmt\"\n\n\t\"charm.land/huh/v2/spinner\"\n)\n\nfunc main() {\n\t_ = spinner.New().Title(\"Loading\").WithAcc"
  },
  {
    "path": "examples/ssh-form/main.go",
    "chars": 3371,
    "preview": "package main\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\t\"time\"\n\n\ttea \"charm.land/bubble"
  },
  {
    "path": "examples/stickers/main.go",
    "chars": 4554,
    "preview": "package main\n\nimport (\n\t\"charm.land/huh/v2\"\n)\n\nfunc main() {\n\tvar (\n\t\tname    string\n\t\taddress string\n\t\tcountry string\n\t"
  },
  {
    "path": "examples/theme/main.go",
    "chars": 1523,
    "preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"charm.land/huh/v2\"\n)\n\nvar themes = map[string]huh.Theme{\n\t\"default\":    huh.Theme"
  },
  {
    "path": "examples/theme/theme.tape",
    "chars": 445,
    "preview": "Output theme.gif\n\nSet Width 800\nSet Height 740\nSet Padding 80\n\nHide\nType \"go build -o theme .\"\nEnter\nCtrl+L\nSleep 500ms\n"
  },
  {
    "path": "examples/timer/main.go",
    "chars": 5157,
    "preview": "package main\n\nimport (\n\t\"log\"\n\t\"strings\"\n\t\"time\"\n\n\t\"charm.land/bubbles/v2/progress\"\n\ttea \"charm.land/bubbletea/v2\"\n\t\"cha"
  },
  {
    "path": "field_confirm.go",
    "chars": 9177,
    "preview": "package huh\n\nimport (\n\t\"cmp\"\n\t\"io\"\n\t\"strings\"\n\n\t\"charm.land/bubbles/v2/key\"\n\ttea \"charm.land/bubbletea/v2\"\n\t\"charm.land/"
  },
  {
    "path": "field_filepicker.go",
    "chars": 10830,
    "preview": "package huh\n\nimport (\n\t\"cmp\"\n\t\"errors\"\n\t\"io\"\n\t\"os\"\n\t\"strings\"\n\n\txstrings \"github.com/charmbracelet/x/exp/strings\"\n\n\t\"cha"
  },
  {
    "path": "field_input.go",
    "chars": 14565,
    "preview": "package huh\n\nimport (\n\t\"cmp\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n\n\t\"charm.land/bubbles/v2/key\"\n\t\"charm.land/bubbles/v2/tex"
  },
  {
    "path": "field_multiselect.go",
    "chars": 22284,
    "preview": "package huh\n\nimport (\n\t\"cmp\"\n\t\"fmt\"\n\t\"io\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\t\"charm.land/bubbles/v2/key\"\n\t\"charm.land/bubble"
  },
  {
    "path": "field_note.go",
    "chars": 8882,
    "preview": "package huh\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)\n\n// Note is"
  },
  {
    "path": "field_select.go",
    "chars": 22549,
    "preview": "package huh\n\nimport (\n\t\"cmp\"\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n\t\"time\"\n\n\t\"charm.land/bubbles/v2/key\"\n\t\"charm.land/bubbles/v2/spinn"
  },
  {
    "path": "field_text.go",
    "chars": 13173,
    "preview": "package huh\n\nimport (\n\t\"cmp\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\n\t\"charm.land/bubbles/v2/key\"\n\t\"charm.l"
  },
  {
    "path": "form.go",
    "chars": 17001,
    "preview": "package huh\n\nimport (\n\t\"cmp\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"sync\"\n\t\"time\"\n\n\t\"charm.land/bubbles/v2/help\"\n\t\"ch"
  },
  {
    "path": "go.mod",
    "chars": 1438,
    "preview": "module charm.land/huh/v2\n\ngo 1.25.8\n\nrequire (\n\tcharm.land/bubbles/v2 v2.0.0\n\tcharm.land/bubbletea/v2 v2.0.2\n\tcharm.land"
  },
  {
    "path": "go.sum",
    "chars": 5966,
    "preview": "charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s=\ncharm.land/bubbles/v2 v2.0.0/go.mod h1:rCHo"
  },
  {
    "path": "group.go",
    "chars": 9929,
    "preview": "package huh\n\nimport (\n\t\"strings\"\n\n\t\"charm.land/bubbles/v2/help\"\n\t\"charm.land/bubbles/v2/viewport\"\n\ttea \"charm.land/bubbl"
  },
  {
    "path": "huh.go",
    "chars": 90,
    "preview": "// Package huh provides components to build terminal-based forms and prompts.\npackage huh\n"
  },
  {
    "path": "huh_test.go",
    "chars": 38693,
    "preview": "package huh\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"regexp\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\ttea \"charm.la"
  },
  {
    "path": "internal/accessibility/accessibility.go",
    "chars": 3689,
    "preview": "// Package accessibility provides accessible functions to capture user input.\npackage accessibility\n\nimport (\n\t\"bufio\"\n\t"
  },
  {
    "path": "internal/compat/model.go",
    "chars": 836,
    "preview": "// Package compat provides common types used across the application.\npackage compat\n\nimport tea \"charm.land/bubbletea/v2"
  },
  {
    "path": "internal/selector/selector.go",
    "chars": 2134,
    "preview": "// Package selector provides a helper type for selecting items.\npackage selector\n\n// Selector is a helper type for selec"
  },
  {
    "path": "keymap.go",
    "chars": 8599,
    "preview": "package huh\n\nimport \"charm.land/bubbles/v2/key\"\n\n// KeyMap is the keybindings to navigate the form.\ntype KeyMap struct {"
  },
  {
    "path": "layout.go",
    "chars": 3659,
    "preview": "package huh\n\nimport (\n\t\"strings\"\n\n\t\"charm.land/lipgloss/v2\"\n)\n\n// A Layout is responsible for laying out groups in a for"
  },
  {
    "path": "option.go",
    "chars": 819,
    "preview": "package huh\n\nimport \"fmt\"\n\n// Option is an option for select fields.\ntype Option[T comparable] struct {\n\tKey      string"
  },
  {
    "path": "run.go",
    "chars": 203,
    "preview": "package huh\n\n// Run runs a single field by wrapping it within a group and a form.\nfunc Run(field Field) error {\n\tgroup :"
  },
  {
    "path": "spinner/spinner.go",
    "chars": 6687,
    "preview": "// Package spinner provides a loading spinner.\npackage spinner\n\nimport (\n\t\"cmp\"\n\t\"context\"\n\t\"io\"\n\t\"os\"\n\t\"strings\"\n\n\t\"cha"
  },
  {
    "path": "spinner/spinner_test.go",
    "chars": 4427,
    "preview": "package spinner\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"io\"\n\t\"reflect\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"charm.land/bubbles/v2/s"
  },
  {
    "path": "theme.go",
    "chars": 14290,
    "preview": "package huh\n\nimport (\n\t\"charm.land/bubbles/v2/help\"\n\t\"charm.land/lipgloss/v2\"\n\tcatppuccin \"github.com/catppuccin/go\"\n)\n\n"
  },
  {
    "path": "validate.go",
    "chars": 1546,
    "preview": "package huh\n\nimport (\n\t\"fmt\"\n\t\"unicode/utf8\"\n)\n\n// ValidateNotEmpty checks if the input is not empty.\nfunc ValidateNotEm"
  },
  {
    "path": "wrap.go",
    "chars": 129,
    "preview": "package huh\n\nimport \"charm.land/lipgloss/v2\"\n\nfunc wrap(s string, limit int) string {\n\treturn lipgloss.Wrap(s, limit, \","
  },
  {
    "path": "zz_resize_width_test.go",
    "chars": 1029,
    "preview": "package huh\n\nimport \"testing\"\n\nfunc TestSelectWithWidthUpdatesViewportWidth(t *testing.T) {\n\tf := NewSelect[string]().\n\t"
  }
]

About this extraction

This page contains the full source code of the charmbracelet/huh GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 108 files (331.5 KB), approximately 101.3k tokens, and a symbol index with 687 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!