[
  {
    "path": ".gitattributes",
    "content": "*.gif filter=lfs diff=lfs merge=lfs -text\n*.png filter=lfs diff=lfs merge=lfs -text\n"
  },
  {
    "path": ".github/CODEOWNERS",
    "content": "* @charmbracelet/everyone\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\n\nupdates:\n  - package-ecosystem: \"gomod\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n      day: \"monday\"\n      time: \"05:00\"\n      timezone: \"America/New_York\"\n    labels:\n      - \"dependencies\"\n    commit-message:\n      prefix: \"chore\"\n      include: \"scope\"\n    groups:\n      all:\n        patterns:\n          - \"*\"\n    ignore:\n      - dependency-name: github.com/charmbracelet/bubbletea/v2\n        versions:\n          - v2.0.0-beta1\n\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n      day: \"monday\"\n      time: \"05:00\"\n      timezone: \"America/New_York\"\n    labels:\n      - \"dependencies\"\n    commit-message:\n      prefix: \"chore\"\n      include: \"scope\"\n    groups:\n      all:\n        patterns:\n          - \"*\"\n\n  - package-ecosystem: \"docker\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n      day: \"monday\"\n      time: \"05:00\"\n      timezone: \"America/New_York\"\n    labels:\n      - \"dependencies\"\n    commit-message:\n      prefix: \"chore\"\n      include: \"scope\"\n    groups:\n      all:\n        patterns:\n          - \"*\"\n\n  - package-ecosystem: \"gomod\"\n    directory: \"/examples\"\n    schedule:\n      interval: \"weekly\"\n      day: \"monday\"\n      time: \"05:00\"\n      timezone: \"America/New_York\"\n    labels:\n      - \"dependencies\"\n    commit-message:\n      prefix: \"chore\"\n      include: \"scope\"\n    groups:\n      all:\n        patterns:\n          - \"*\"\n    ignore:\n      - dependency-name: github.com/charmbracelet/bubbletea/v2\n        versions:\n          - v2.0.0-beta1\n\n  - package-ecosystem: \"gomod\"\n    directory: \"/spinner\"\n    schedule:\n      interval: \"weekly\"\n      day: \"monday\"\n      time: \"05:00\"\n      timezone: \"America/New_York\"\n    labels:\n      - \"dependencies\"\n    commit-message:\n      prefix: \"chore\"\n      include: \"scope\"\n    groups:\n      all:\n        patterns:\n          - \"*\"\n    ignore:\n      - dependency-name: github.com/charmbracelet/bubbletea/v2\n        versions:\n          - v2.0.0-beta1\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: build\non: [push, pull_request]\njobs:\n  build:\n    uses: charmbracelet/meta/.github/workflows/build.yml@main\n\n  build-go-mod:\n    uses: charmbracelet/meta/.github/workflows/build.yml@main\n    with:\n      go-version: \"\"\n      go-version-file: ./go.mod\n\n  build-examples:\n    uses: charmbracelet/meta/.github/workflows/build.yml@main\n    with:\n      go-version: \"\"\n      go-version-file: ./examples/go.mod\n      working-directory: ./examples\n"
  },
  {
    "path": ".github/workflows/dependabot-sync.yml",
    "content": "name: dependabot-sync\non:\n  schedule:\n    - cron: \"0 0 * * 0\" # every Sunday at midnight\n  workflow_dispatch: # allows manual triggering\n\npermissions:\n  contents: write\n  pull-requests: write\n\njobs:\n  dependabot-sync:\n    uses: charmbracelet/meta/.github/workflows/dependabot-sync.yml@main\n    with:\n      repo_name: ${{ github.event.repository.name }}\n    secrets:\n      gh_token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/lint-sync.yml",
    "content": "name: lint-sync\non:\n  schedule:\n    # every Sunday at midnight\n    - cron: \"0 0 * * 0\"\n  workflow_dispatch: # allows manual triggering\n\npermissions:\n  contents: write\n  pull-requests: write\n\njobs:\n  lint:\n    uses: charmbracelet/meta/.github/workflows/lint-sync.yml@main\n"
  },
  {
    "path": ".github/workflows/lint.yml",
    "content": "name: lint\non:\n  push:\n  pull_request:\n\njobs:\n  lint:\n    uses: charmbracelet/meta/.github/workflows/lint.yml@main\n    with:\n      golangci_path: .golangci.yml\n      timeout: 10m\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: goreleaser\n\non:\n  push:\n    tags:\n      - v*.*.*\n\nconcurrency:\n  group: goreleaser\n  cancel-in-progress: true\n\njobs:\n  goreleaser:\n    uses: charmbracelet/meta/.github/workflows/goreleaser.yml@main\n    secrets:\n      docker_username: ${{ secrets.DOCKERHUB_USERNAME }}\n      docker_token: ${{ secrets.DOCKERHUB_TOKEN }}\n      gh_pat: ${{ secrets.PERSONAL_ACCESS_TOKEN }}\n      goreleaser_key: ${{ secrets.GORELEASER_KEY }}\n      twitter_consumer_key: ${{ secrets.TWITTER_CONSUMER_KEY }}\n      twitter_consumer_secret: ${{ secrets.TWITTER_CONSUMER_SECRET }}\n      twitter_access_token: ${{ secrets.TWITTER_ACCESS_TOKEN }}\n      twitter_access_token_secret: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }}\n      mastodon_client_id: ${{ secrets.MASTODON_CLIENT_ID }}\n      mastodon_client_secret: ${{ secrets.MASTODON_CLIENT_SECRET }}\n      mastodon_access_token: ${{ secrets.MASTODON_ACCESS_TOKEN }}\n      discord_webhook_id: ${{ secrets.DISCORD_WEBHOOK_ID }}\n      discord_webhook_token: ${{ secrets.DISCORD_WEBHOOK_TOKEN }}\n# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json\n"
  },
  {
    "path": ".gitignore",
    "content": "# If you prefer the allow list template instead of the deny list, see community template:\n# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore\n#\n# Binaries for programs and plugins\n*.exe\n*.exe~\n*.dll\n*.so\n*.dylib\n\n# Test binary, built with `go test -c`\n*.test\n\n# Output of the go coverage tool, specifically when used with LiteIDE\n*.out\n\n# Dependency directories (remove the comment below to include it)\n# vendor/\n\n# Go workspace file\ngo.work\n\n# Debugging\ndebug.log\n"
  },
  {
    "path": ".golangci.yml",
    "content": "version: \"2\"\nrun:\n  tests: false\nlinters:\n  enable:\n    - bodyclose\n    - exhaustive\n    - goconst\n    - godot\n    - gomoddirectives\n    - goprintffuncname\n    - gosec\n    - misspell\n    - nakedret\n    - nestif\n    - nilerr\n    - noctx\n    - nolintlint\n    - prealloc\n    - revive\n    - rowserrcheck\n    - sqlclosecheck\n    - tparallel\n    - unconvert\n    - unparam\n    - whitespace\n    - wrapcheck\n  exclusions:\n    rules:\n      - text: '(slog|log)\\.\\w+'\n        linters:\n          - noctx\n    generated: lax\n    presets:\n      - common-false-positives\n  settings:\n    exhaustive:\n      default-signifies-exhaustive: true\nissues:\n  max-issues-per-linter: 0\n  max-same-issues: 0\nformatters:\n  enable:\n    - gofumpt\n    - goimports\n  exclusions:\n    generated: lax\n"
  },
  {
    "path": ".goreleaser.yml",
    "content": "includes:\n  - from_url:\n      url: charmbracelet/meta/main/goreleaser-lib.yaml\n\n# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json\n\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2023–2026 Charm\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": ".PHONY: spinner\n\n$(V).SILENT:\ntest:\n\tgo test ./...\n\nspinner:\n\tcd spinner/examples/loading && go run .\n\nburger:\n\tcd examples/burger && go run .\n\ntheme:\n\tcd examples/theme && go run .\n\ngh:\n\tcd examples/gh && go run .\n"
  },
  {
    "path": "README.md",
    "content": "# 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/charmbracelet/huh/releases\"><img src=\"https://img.shields.io/github/release/charmbracelet/huh.svg\" alt=\"Latest Release\"></a>\n  <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>\n  <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>\n</p>\n\nA simple, powerful library for building interactive forms and prompts in the terminal.\n\n<img alt=\"Running a burger form\" width=\"600\" src=\"https://vhs.charm.sh/vhs-3J4i6HE3yBmz6SUO3HqILr.gif\">\n\n`huh?` is easy to use in a standalone fashion, can be\n[integrated into a Bubble Tea application](#what-about-bubble-tea), and contains\na first-class [accessible mode](#accessibility) for screen readers.\n\nThe above example is running from a single Go program ([source](./examples/burger/main.go)).\n\n## Tutorial\n\nLet’s build a form for ordering burgers. To start, we’ll import the library and\ndefine a few variables where we'll store answers.\n\n```go\npackage main\n\nimport \"charm.land/huh/v2\"\n\nvar (\n    burger       string\n    toppings     []string\n    sauceLevel   int\n    name         string\n    instructions string\n    discount     bool\n)\n```\n\n`huh?` separates forms into groups (you can think of groups as pages). Groups\nare made of fields (e.g. `Select`, `Input`, `Text`). We will set up three\ngroups for the customer to fill out.\n\n```go\nform := huh.NewForm(\n    huh.NewGroup(\n        // Ask the user for a base burger and toppings.\n        huh.NewSelect[string]().\n            Title(\"Choose your burger\").\n            Options(\n                huh.NewOption(\"Charmburger Classic\", \"classic\"),\n                huh.NewOption(\"Chickwich\", \"chickwich\"),\n                huh.NewOption(\"Fishburger\", \"fishburger\"),\n                huh.NewOption(\"Charmpossible™ Burger\", \"charmpossible\"),\n            ).\n            Value(&burger), // store the chosen option in the \"burger\" variable\n\n        // Let the user select multiple toppings.\n        huh.NewMultiSelect[string]().\n            Title(\"Toppings\").\n            Options(\n                huh.NewOption(\"Lettuce\", \"lettuce\").Selected(true),\n                huh.NewOption(\"Tomatoes\", \"tomatoes\").Selected(true),\n                huh.NewOption(\"Jalapeños\", \"jalapeños\"),\n                huh.NewOption(\"Cheese\", \"cheese\"),\n                huh.NewOption(\"Vegan Cheese\", \"vegan cheese\"),\n                huh.NewOption(\"Nutella\", \"nutella\"),\n            ).\n            Limit(4). // there’s a 4 topping limit!\n            Value(&toppings),\n\n        // Option values in selects and multi selects can be any type you\n        // want. We’ve been recording strings above, but here we’ll store\n        // answers as integers. Note the generic \"[int]\" directive below.\n        huh.NewSelect[int]().\n            Title(\"How much Charm Sauce do you want?\").\n            Options(\n                huh.NewOption(\"None\", 0),\n                huh.NewOption(\"A little\", 1),\n                huh.NewOption(\"A lot\", 2),\n            ).\n            Value(&sauceLevel),\n    ),\n\n    // Gather some final details about the order.\n    huh.NewGroup(\n        huh.NewInput().\n            Title(\"What’s your name?\").\n            Value(&name).\n            // Validating fields is easy. The form will mark erroneous fields\n            // and display error messages accordingly.\n            Validate(func(str string) error {\n                if str == \"Frank\" {\n                    return errors.New(\"Sorry, we don’t serve customers named Frank.\")\n                }\n                return nil\n            }),\n\n        huh.NewText().\n            Title(\"Special Instructions\").\n            CharLimit(400).\n            Value(&instructions),\n\n        huh.NewConfirm().\n            Title(\"Would you like 15% off?\").\n            Value(&discount),\n    ),\n)\n```\n\nFinally, run the form:\n\n```go\nerr := form.Run()\nif err != nil {\n    log.Fatal(err)\n}\n\nif !discount {\n    fmt.Println(\"What? You didn’t take the discount?!\")\n}\n```\n\nAnd that’s it! For more info see [the full source][burgersource] for this\nexample as well as [the docs][docs].\n\nIf you need more dynamic forms that change based on input from previous fields,\ncheck out the [dynamic forms](#dynamic-forms) example.\n\n[burgersource]: ./examples/burger/main.go\n[docs]: https://pkg.go.dev/charm.land/huh/v2?tab=doc\n\n## Field Reference\n\n- [`Input`](#input): single line text input\n- [`Text`](#text): multi-line text input\n- [`Select`](#select): select an option from a list\n- [`MultiSelect`](#multiple-select): select multiple options from a list\n- [`Confirm`](#confirm): confirm an action (yes or no)\n\n> [!TIP]\n> Just want to prompt the user with a single field? Each field has a `Run`\n> method that can be used as a shorthand for gathering quick and easy input.\n\n```go\nvar name string\n\nhuh.NewInput().\n    Title(\"What’s your name?\").\n    Value(&name).\n    Run() // this is blocking...\n\nfmt.Printf(\"Hey, %s!\\n\", name)\n```\n\n### Input\n\nPrompt the user for a single line of text.\n\n<img alt=\"Input field\" width=\"600\" src=\"https://vhs.charm.sh/vhs-1ULe9JbTHfwFmm3hweRVtD.gif\">\n\n```go\nhuh.NewInput().\n    Title(\"What’s for lunch?\").\n    Prompt(\"?\").\n    Validate(isFood).\n    Value(&lunch)\n```\n\n### Text\n\nPrompt the user for multiple lines of text.\n\n<img alt=\"Text field\" width=\"600\" src=\"https://vhs.charm.sh/vhs-2rrIuVSEf38bT0cwc8hfEG.gif\">\n\n```go\nhuh.NewText().\n    Title(\"Tell me a story.\").\n    Validate(checkForPlagiarism).\n    Value(&story)\n```\n\n### Select\n\nPrompt the user to select a single option from a list.\n\n<img alt=\"Select field\" width=\"600\" src=\"https://vhs.charm.sh/vhs-7wFqZlxMWgbWmOIpBqXJTi.gif\">\n\n```go\nhuh.NewSelect[string]().\n    Title(\"Pick a country.\").\n    Options(\n        huh.NewOption(\"United States\", \"US\"),\n        huh.NewOption(\"Germany\", \"DE\"),\n        huh.NewOption(\"Brazil\", \"BR\"),\n        huh.NewOption(\"Canada\", \"CA\"),\n    ).\n    Value(&country)\n```\n\n### Multiple Select\n\nPrompt the user to select multiple (zero or more) options from a list.\n\n<img alt=\"Multiselect field\" width=\"600\" src=\"https://vhs.charm.sh/vhs-3TLImcoexOehRNLELysMpK.gif\">\n\n```go\nhuh.NewMultiSelect[string]().\n    Options(\n        huh.NewOption(\"Lettuce\", \"Lettuce\").Selected(true),\n        huh.NewOption(\"Tomatoes\", \"Tomatoes\").Selected(true),\n        huh.NewOption(\"Charm Sauce\", \"Charm Sauce\"),\n        huh.NewOption(\"Jalapeños\", \"Jalapeños\"),\n        huh.NewOption(\"Cheese\", \"Cheese\"),\n        huh.NewOption(\"Vegan Cheese\", \"Vegan Cheese\"),\n        huh.NewOption(\"Nutella\", \"Nutella\"),\n    ).\n    Title(\"Toppings\").\n    Limit(4).\n    Value(&toppings)\n```\n\n### Confirm\n\nPrompt the user to confirm (Yes or No).\n\n<img alt=\"Confirm field\" width=\"600\" src=\"https://vhs.charm.sh/vhs-2HeX5MdOxLsrWwsa0TNMIL.gif\">\n\n```go\nhuh.NewConfirm().\n    Title(\"Are you sure?\").\n    Affirmative(\"Yes!\").\n    Negative(\"No.\").\n    Value(&confirm)\n```\n\n## Accessibility\n\n`huh?` has a special rendering option designed specifically for screen readers.\nYou can enable it with `form.WithAccessible(true)`.\n\n> [!TIP]\n> We recommend setting this through an environment variable or configuration\n> option to allow the user to control accessibility.\n\n```go\naccessibleMode := os.Getenv(\"ACCESSIBLE\") != \"\"\nform.WithAccessible(accessibleMode)\n```\n\nAccessible forms will drop TUIs in favor of standard prompts, providing better\ndictation and feedback of the information on screen for the visually impaired.\n\n<img alt=\"Accessible cuisine form\" width=\"600\" src=\"https://vhs.charm.sh/vhs-19xEBn4LgzPZDtgzXRRJYS.gif\">\n\n## Themes\n\n`huh?` contains a powerful theme abstraction. Supply your own custom theme or\nchoose from one of the five predefined themes:\n\n- `Charm`\n- `Dracula`\n- `Catppuccin`\n- `Base 16`\n- `Default`\n\n<br />\n<p>\n    <img alt=\"Charm-themed form\" width=\"400\" src=\"https://stuff.charm.sh/huh/themes/charm-theme.png\">\n    <img alt=\"Dracula-themed form\" width=\"400\" src=\"https://stuff.charm.sh/huh/themes/dracula-theme.png\">\n    <img alt=\"Catppuccin-themed form\" width=\"400\" src=\"https://stuff.charm.sh/huh/themes/catppuccin-theme.png\">\n    <img alt=\"Base 16-themed form\" width=\"400\" src=\"https://stuff.charm.sh/huh/themes/basesixteen-theme.png\">\n    <img alt=\"Default-themed form\" width=\"400\" src=\"https://stuff.charm.sh/huh/themes/default-theme.png\">\n</p>\n\nThemes can take advantage of the full range of\n[Lip Gloss][lipgloss] style options. For a high level theme reference see\n[the docs](https://pkg.go.dev/charm.land/huh/v2#Theme).\n\n[lipgloss]: https://github.com/charmbracelet/lipgloss\n\n## Dynamic Forms\n\n`huh?` forms can be as dynamic as your heart desires. Simply replace properties\nwith their equivalent `Func` to recompute the properties value every time a\ndifferent part of your form changes.\n\nHere’s how you would build a simple country + state / province picker.\n\nFirst, define some variables that we’ll use to store the user selection.\n\n```go\nvar country string\nvar state string\n```\n\nDefine your country select as you normally would:\n\n```go\nhuh.NewSelect[string]().\n    Options(huh.NewOptions(\"United States\", \"Canada\", \"Mexico\")...).\n    Value(&country).\n    Title(\"Country\").\n```\n\nDefine your state select with `TitleFunc` and `OptionsFunc` instead of `Title`\nand `Options`. This will allow you to change the title and options based on the\nselection of the previous field, i.e. `country`.\n\nTo do this, we provide a `func() string` and a `binding any` to `TitleFunc`. The\nfunction defines what to show for the title and the binding specifies what value\nneeds to change for the function to recompute. So if `country` changes (e.g. the\nuser changes the selection) we will recompute the function.\n\nFor `OptionsFunc`, we provide a `func() []Option[string]` and a `binding any`.\nWe’ll fetch the country’s states, provinces, or territories from an API. `huh`\nwill automatically handle caching for you.\n\n> [!IMPORTANT]\n> We have to pass `&country` as the binding to recompute the function only when\n> `country` changes, otherwise we will hit the API too often.\n\n```go\nhuh.NewSelect[string]().\n    Value(&state).\n    Height(8).\n    TitleFunc(func() string {\n        switch country {\n        case \"United States\":\n            return \"State\"\n        case \"Canada\":\n            return \"Province\"\n        default:\n            return \"Territory\"\n        }\n    }, &country).\n    OptionsFunc(func() []huh.Option[string] {\n        opts := fetchStatesForCountry(country)\n        return huh.NewOptions(opts...)\n    }, &country),\n```\n\nLastly, run the `form` with these inputs.\n\n```go\nerr := form.Run()\nif err != nil {\n    log.Fatal(err)\n}\n```\n\n<img width=\"600\" src=\"https://vhs.charm.sh/vhs-6FRmBjNi2aiRb4INPXwIjo.gif\" alt=\"Country / State form with dynamic inputs running.\">\n\n## Bonus: Spinner\n\n`huh?` ships with a standalone spinner package. It’s useful for indicating\nbackground activity after a form is submitted.\n\n<img alt=\"Spinner while making a burger\" width=\"600\" src=\"https://vhs.charm.sh/vhs-6HvYomAFP6H8mngOYWXvwJ.gif\">\n\nCreate a new spinner, set a title, set the action (or provide a `Context`), and run the spinner:\n\n<table>\n\n<tr>\n<td> <strong>Action Style</strong> </td><td> <strong>Context Style</strong> </td></tr>\n<tr>\n<td>\n\n```go\nerr := spinner.New().\n    Title(\"Making your burger...\").\n    Action(makeBurger).\n    Run()\n\nfmt.Println(\"Order up!\")\n```\n\n</td>\n<td>\n\n```go\ngo makeBurger()\n\nerr := spinner.New().\n    Type(spinner.Line).\n    Title(\"Making your burger...\").\n    Context(ctx).\n    Run()\n\nfmt.Println(\"Order up!\")\n```\n\n</td>\n</tr>\n</table>\n\nFor more on Spinners see the [spinner examples](./spinner/examples) and\n[the spinner docs](https://pkg.go.dev/charm.land/huh/v2/spinner).\n\n## What about Bubble Tea?\n\n<img alt=\"Bubbletea + Huh?\" width=\"174\" src=\"https://stuff.charm.sh/huh/bubbletea-huh.png\">\n\nHuh is built on [Bubble Tea][tea] and, in addition to its standalone mode,\n`huh?` has first-class support and can be easily integrated into\nBubble Tea applications. It’s very useful in portions of your Bubble Tea\napplication that need form-like input, and for times when you need more\nflexibility than `huh?` alone can offer.\n\n<img alt=\"Bubble Tea embedded form example\" width=\"800\" src=\"https://vhs.charm.sh/vhs-3wGaB7EUKWmojeaHpARMUv.gif\">\n\nA `huh.Form` is just a `tea.Model`, so you can use it just as\nyou would any other [Bubble](https://github.com/charmbracelet/bubbles).\n\n```go\ntype Model struct {\n    form *huh.Form // huh.Form is just a tea.Model\n}\n\nfunc NewModel() Model {\n    return Model{\n        form: huh.NewForm(\n            huh.NewGroup(\n                huh.NewSelect[string]().\n                    Key(\"class\").\n                    Options(huh.NewOptions(\"Warrior\", \"Mage\", \"Rogue\")...).\n                    Title(\"Choose your class\"),\n\n            huh.NewSelect[int]().\n                Key(\"level\").\n                Options(huh.NewOptions(1, 20, 9999)...).\n                Title(\"Choose your level\"),\n            ),\n        )\n    }\n}\n\nfunc (m Model) Init() tea.Cmd {\n    return m.form.Init()\n}\n\nfunc (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {\n    // ...\n\n    form, cmd := m.form.Update(msg)\n    if f, ok := form.(*huh.Form); ok {\n        m.form = f\n    }\n\n    return m, cmd\n}\n\nfunc (m Model) View() string {\n    if m.form.State == huh.StateCompleted {\n        class := m.form.GetString(\"class\")\n        level := m.form.GetInt(\"level\")\n        return fmt.Sprintf(\"You selected: %s, Lvl. %d\", class, level)\n    }\n    return m.form.View()\n}\n\n```\n\nFor more info in using `huh?` in Bubble Tea applications see [the full Bubble\nTea example][example].\n\n[tea]: https://github.com/charmbracelet/bubbletea\n[bubbles]: https://github.com/charmbracelet/bubbles\n[example]: https://github.com/charmbracelet/huh/blob/main/examples/bubbletea/main.go\n\n## `Huh?` in the Wild\n\nFor some `Huh?` programs in production, see:\n\n* [glyphs](https://github.com/maaslalani/glyphs): a unicode symbol picker\n* [meteor](https://github.com/stefanlogue/meteor): a highly customisable conventional commit message tool\n* [freeze](https://github.com/charmbracelet/freeze): a tool for generating images of code and terminal output\n* [savvy](https://github.com/getsavvyinc/savvy-cli): the easiest way to create, share, and run runbooks in the terminal\n\n## Contributing\n\nSee [contributing][contribute].\n\n[contribute]: https://github.com/charmbracelet/huh/contribute\n\n## Feedback\n\nWe’d love to hear your thoughts on this project. Feel free to drop us a note!\n\n- [Twitter](https://twitter.com/charmcli)\n- [The Fediverse](https://mastodon.social/@charmcli)\n- [Discord](https://charm.sh/chat)\n\n## Acknowledgments\n\n`huh?` is inspired by the wonderful [Survey][survey] library by Alec Aivazis.\n\n[survey]: https://github.com/AlecAivazis/survey\n\n## License\n\n[MIT](https://github.com/charmbracelet/bubbletea/raw/master/LICENSE)\n\n---\n\nPart of [Charm](https://charm.sh).\n\n<a href=\"https://charm.land/\"><img alt=\"The Charm logo\" src=\"https://stuff.charm.sh/charm-banner-next.jpg\" width=\"400\"></a>\n\nCharm热爱开源 • Charm loves open source • نحنُ نحب المصادر المفتوحة\n"
  },
  {
    "path": "UPGRADE_GUIDE_V2.md",
    "content": "# Huh v2 Upgrade Guide\n\nThis guide will help you migrate from Huh v1 to v2. Most changes are straightforward, and many are handled automatically by your IDE or `gofmt`.\n\n> [!TIP]\n> For a high-level overview of what's new, check out [What's New in Huh v2](WHATS_NEW_V2.md).\n\n## Quick Start\n\nUpdate your imports and dependencies, and you're 90% done:\n\n```bash\n# Update your go.mod\ngo get charm.land/huh/v2@latest\ngo get charm.land/bubbletea/v2@latest\ngo get charm.land/lipgloss/v2@latest\ngo get charm.land/bubbles/v2@latest\n```\n\nThen update your import paths:\n\n```go\n// Before\nimport (\n    \"github.com/charmbracelet/huh\"\n    \"github.com/charmbracelet/huh/spinner\"\n    tea \"github.com/charmbracelet/bubbletea\"\n    \"github.com/charmbracelet/lipgloss\"\n    \"github.com/charmbracelet/bubbles/key\"\n)\n\n// After\nimport (\n    \"charm.land/huh/v2\"\n    \"charm.land/huh/v2/spinner\"\n    tea \"charm.land/bubbletea/v2\"\n    \"charm.land/lipgloss/v2\"\n    \"charm.land/bubbles/v2/key\"\n)\n```\n\n## Breaking Changes\n\n### Import Paths\n\nAll Charm imports now use the `charm.land` vanity domain with a `/v2` version suffix.\n\n| v1 | v2 |\n|----|-----|\n| `github.com/charmbracelet/huh` | `charm.land/huh/v2` |\n| `github.com/charmbracelet/huh/spinner` | `charm.land/huh/v2/spinner` |\n| `github.com/charmbracelet/bubbletea` | `charm.land/bubbletea/v2` |\n| `github.com/charmbracelet/lipgloss` | `charm.land/lipgloss/v2` |\n| `github.com/charmbracelet/bubbles` | `charm.land/bubbles/v2` |\n\n### Theme Changes\n\nThemes are now passed by value and take a `bool` parameter for dark mode detection.\n\n**Before:**\n```go\nform := huh.NewForm(\n    // ...\n).WithTheme(huh.ThemeCharm())\n```\n\n**After:**\n```go\nisDark := lipgloss.HasDarkBackground() // or detect however you prefer\nform := huh.NewForm(\n    // ...\n).WithTheme(huh.ThemeCharm(isDark))\n```\n\nAll built-in themes now follow this pattern:\n\n```go\nhuh.ThemeCharm(isDark bool) *Styles\nhuh.ThemeDracula(isDark bool) *Styles\nhuh.ThemeCatppuccin(isDark bool) *Styles\nhuh.ThemeBase(isDark bool) *Styles\nhuh.ThemeBase16(isDark bool) *Styles\n```\n\n### Theme Type Changes\n\nThe `Theme` type has changed from a struct to an interface:\n\n**Before:**\n```go\ntype Theme struct {\n    Form FormStyles\n    Group GroupStyles\n    FieldSeparator lipgloss.Style\n    Blurred FieldStyles\n    Focused FieldStyles\n    Help help.Styles\n}\n```\n\n**After:**\n```go\ntype ThemeFunc func(isDark bool) *Styles\n```\n\nIf you created custom themes, you'll need to update them to this new function signature:\n\n```go\nfunc MyCustomTheme(isDark bool) *Styles {\n    styles := &huh.Styles{\n        // Your custom styles...\n    }\n    return styles\n}\n```\n\n### Field-Level WithAccessible Removed\n\nIndividual fields no longer have `WithAccessible()` methods. Accessible mode is now controlled exclusively at the form level, making it simpler and more consistent.\n\n**Before:**\n```go\n// v1 - each field could have its own accessible setting\ninput := huh.NewInput().\n    Title(\"Name\").\n    WithAccessible(true)  // ❌ No longer exists\n\nselect := huh.NewSelect[string]().\n    Title(\"Country\").\n    Options(huh.NewOptions(\"US\", \"CA\", \"MX\")...).\n    WithAccessible(true)  // ❌ Removed\n\nconfirm := huh.NewConfirm().\n    Title(\"Continue?\").\n    WithAccessible(true)  // ❌ Gone from all field types\n\nform := huh.NewForm(\n    huh.NewGroup(input, select, confirm),\n).WithAccessible(true)\n```\n\n**After:**\n```go\n// v2 - only the form controls accessible mode\ninput := huh.NewInput().\n    Title(\"Name\")\n\nselect := huh.NewSelect[string]().\n    Title(\"Country\").\n    Options(huh.NewOptions(\"US\", \"CA\", \"MX\")...)\n\nconfirm := huh.NewConfirm().\n    Title(\"Continue?\")\n\nform := huh.NewForm(\n    huh.NewGroup(input, select, confirm),\n).WithAccessible(true)  // ✅ One setting for all fields\n```\n\n**Fields affected:**\n- `Input.WithAccessible()` - removed\n- `Text.WithAccessible()` - removed\n- `Select.WithAccessible()` - removed\n- `MultiSelect.WithAccessible()` - removed\n- `Confirm.WithAccessible()` - removed\n- `Note.WithAccessible()` - removed\n- `FilePicker.WithAccessible()` - removed\n\nThe separate `github.com/charmbracelet/huh/accessibility` package is also gone. Just use `Form.WithAccessible()` directly.\n\n### Bubble Tea v2 Integration\n\nAll methods that returned or accepted Bubble Tea types have been updated to v2:\n\n**Field Methods:**\n- `Blur() tea.Cmd` (now returns `charm.land/bubbletea/v2.Cmd`)\n- `Focus() tea.Cmd` (now returns `charm.land/bubbletea/v2.Cmd`)\n- `Init() tea.Cmd` (now returns `charm.land/bubbletea/v2.Cmd`)\n- `Update(tea.Msg) (tea.Model, tea.Cmd)` (now uses v2 types)\n\n**Form Methods:**\n- `Init() tea.Cmd` (now returns `charm.land/bubbletea/v2.Cmd`)\n- `Update(tea.Msg) (tea.Model, tea.Cmd)` (now uses v2 types)\n- `WithProgramOptions(...tea.ProgramOption)` (now uses v2 types)\n\n**Key Bindings:**\n- `KeyBinds() []key.Binding` (now returns `charm.land/bubbles/v2/key.Binding`)\n\nThese changes are mostly mechanical. Your IDE should help you update these automatically.\n\n### Lip Gloss v2 Types\n\nAll Lip Gloss types have been updated to v2. This affects style definitions in custom themes:\n\n**Before:**\n```go\nimport \"github.com/charmbracelet/lipgloss\"\n\nstyle := lipgloss.NewStyle().\n    Foreground(lipgloss.Color(\"205\"))\n```\n\n**After:**\n```go\nimport \"charm.land/lipgloss/v2\"\n\nstyle := lipgloss.NewStyle().\n    Foreground(lipgloss.Color(\"205\"))\n```\n\nThe API is largely the same, but the import path and internal types have changed.\n\n### Position Type\n\nButton alignment now uses Lip Gloss v2's `Position` type:\n\n**Before:**\n```go\nimport \"github.com/charmbracelet/lipgloss\"\n\nfield.WithButtonAlignment(lipgloss.Left)\n```\n\n**After:**\n```go\nimport \"charm.land/lipgloss/v2\"\n\nfield.WithButtonAlignment(lipgloss.Left)\n```\n\n## New Features\n\n### View Hooks\n\nYou can now modify the view before it's rendered:\n\n```go\nform.WithViewHook(func(v tea.View) tea.View {\n    // Modify view properties like alt screen, mouse mode, etc.\n    v.AltScreen = true\n    return v\n})\n```\n\n### Width Method\n\nSelect and MultiSelect fields now expose a `Width()` method for getting the field's width:\n\n```go\nwidth := multiSelect.Width()\n```\n\n### Model Type\n\nThe `Model` type is now exported, improving type safety when working with forms in Bubble Tea applications:\n\n```go\nvar _ tea.Model = (*huh.Model)(nil)\n```\n\n## Migration Checklist\n\n- [ ] Update `go.mod` dependencies to v2\n- [ ] Update all import paths from `github.com/charmbracelet/` to `charm.land/` with `/v2` suffix\n- [ ] Update theme calls to pass `isDark bool` parameter\n- [ ] Remove field-level `WithAccessible()` calls (e.g., from `Input`, `Select`, etc.)\n- [ ] Keep form-level `WithAccessible()` calls (those still work)\n- [ ] Remove imports from `github.com/charmbracelet/huh/accessibility` package\n- [ ] Update custom themes to `ThemeFunc` signature if applicable\n- [ ] Run `go mod tidy`\n- [ ] Run tests\n- [ ] Update any documentation or examples\n\n## Common Issues\n\n### Import Cycles\n\nIf you encounter import cycle issues, make sure all Charm dependencies are on v2:\n\n```bash\ngo list -m all | grep charmbracelet\ngo list -m all | grep charm.land\n```\n\nEnsure nothing is still referencing v1 versions.\n\n### Type Mismatches\n\nIf you see type errors with `tea.Model`, `tea.Msg`, or `tea.Cmd`, double-check your Bubble Tea import:\n\n```go\nimport tea \"charm.land/bubbletea/v2\"  // Make sure it's v2!\n```\n\n### Theme Signature Errors\n\nIf you get errors about theme functions, remember all built-in themes now require a `bool` parameter:\n\n```go\n// ✅ Correct\nform.WithTheme(huh.ThemeCharm(true))\n\n// ❌ Wrong\nform.WithTheme(huh.ThemeCharm())\n```\n\n## Getting Help\n\nIf you run into issues:\n\n- Check the [examples](./examples) directory for reference implementations\n- Read the [Bubble Tea v2 Upgrade Guide](https://github.com/charmbracelet/bubbletea/blob/main/UPGRADE_GUIDE_V2.md)\n- Ask in [Discord](https://charm.land/chat) or [Matrix](https://charm.land/matrix)\n- Open an issue on [GitHub](https://github.com/charmbracelet/huh/issues)\n\n---\n\nPart of [Charm](https://charm.land).\n\n<a href=\"https://charm.land/\"><img alt=\"The Charm logo\" src=\"https://stuff.charm.sh/charm-badge.jpg\" width=\"400\"></a>\n\nCharm热爱开源 • Charm loves open source • نحنُ نحب المصادر المفتوحة\n"
  },
  {
    "path": "accessor.go",
    "content": "package huh\n\n// Accessor give read/write access to field values.\ntype Accessor[T any] interface {\n\tGet() T\n\tSet(value T)\n}\n\n// EmbeddedAccessor is a basic accessor, acting as the default one for fields.\ntype EmbeddedAccessor[T any] struct {\n\tvalue T\n}\n\n// Get gets the value.\nfunc (a *EmbeddedAccessor[T]) Get() T {\n\treturn a.value\n}\n\n// Set sets the value.\nfunc (a *EmbeddedAccessor[T]) Set(value T) {\n\ta.value = value\n}\n\n// PointerAccessor allows field value to be exposed as a pointed variable.\ntype PointerAccessor[T any] struct {\n\tvalue *T\n}\n\n// NewPointerAccessor returns a new pointer accessor.\nfunc NewPointerAccessor[T any](value *T) *PointerAccessor[T] {\n\treturn &PointerAccessor[T]{\n\t\tvalue: value,\n\t}\n}\n\n// Get gets the value.\nfunc (a *PointerAccessor[T]) Get() T {\n\treturn *a.value\n}\n\n// Set sets the value.\nfunc (a *PointerAccessor[T]) Set(value T) {\n\t*a.value = value\n}\n"
  },
  {
    "path": "eval.go",
    "content": "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 cached value and a function to\n// recompute it. It's bindings are what we check to see if we need to recompute\n// the value.\n//\n// By default it is also cached.\ntype Eval[T any] struct {\n\tval T\n\tfn  func() T\n\n\tbindings     any\n\tbindingsHash uint64\n\tcache        map[uint64]T\n\n\tloading      bool\n\tloadingStart time.Time\n}\n\nconst spinnerShowThreshold = 25 * time.Millisecond\n\nfunc hash(val any) uint64 {\n\thash, _ := hashstructure.Hash(val, hashstructure.FormatV2, nil)\n\treturn hash\n}\n\nfunc (e *Eval[T]) shouldUpdate() (bool, uint64) {\n\tif e.fn == nil {\n\t\treturn false, 0\n\t}\n\tnewHash := hash(e.bindings)\n\treturn e.bindingsHash != newHash, newHash\n}\n\nfunc (e *Eval[T]) loadFromCache() bool {\n\tval, ok := e.cache[e.bindingsHash]\n\tif ok {\n\t\te.loading = false\n\t\te.val = val\n\t}\n\treturn ok\n}\n\nfunc (e *Eval[T]) update(val T) {\n\te.val = val\n\te.cache[e.bindingsHash] = val\n\te.loading = false\n}\n\ntype updateTitleMsg struct {\n\tid    int\n\thash  uint64\n\ttitle string\n}\n\ntype updateDescriptionMsg struct {\n\tid          int\n\thash        uint64\n\tdescription string\n}\n\ntype updatePlaceholderMsg struct {\n\tid          int\n\thash        uint64\n\tplaceholder string\n}\n\ntype updateSuggestionsMsg struct {\n\tid          int\n\thash        uint64\n\tsuggestions []string\n}\n\ntype updateOptionsMsg[T comparable] struct {\n\tid      int\n\thash    uint64\n\toptions []Option[T]\n}\n"
  },
  {
    "path": "examples/.gitignore",
    "content": ".ssh\n"
  },
  {
    "path": "examples/accessibility/accessible.tape",
    "content": "Output accessible.gif\n\nSet Height 600\nSet Width 1000\n\nHide\n  Type \"go build -o accessible .\" Enter\n  Type \"export ACCESSIBLE=true\" Enter\n  Type \"clear && ./accessible\"\n  Enter\n  Sleep 1s\nShow\n\nSleep 1s\n\nType \"2\"\nSleep 500ms\nEnter\nSleep 1.5s\n\nType \"Souvlaki\"\nSleep 1.5s\nEnter\nSleep 1.5s\n\nHide\nType \"rm accessible\" Enter\nSleep 1s\n"
  },
  {
    "path": "examples/accessibility/main.go",
    "content": "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.NewSelect[string]().\n\t\t\t\tOptions(huh.NewOptions(\"Italian\", \"Greek\", \"Indian\", \"Japanese\", \"American\")...).\n\t\t\t\tTitle(\"Favorite Cuisine?\"),\n\t\t),\n\n\t\thuh.NewGroup(\n\t\t\thuh.NewInput().\n\t\t\t\tTitle(\"Favorite Meal?\").\n\t\t\t\tPlaceholder(\"Breakfast\"),\n\t\t),\n\t).WithAccessible(true)\n\n\terr := form.Run()\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n"
  },
  {
    "path": "examples/accessibility-secure-input/main.go",
    "content": "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 errors.New(\"input cannot be empty\")\n\t}\n\treturn nil\n}\n\nfunc main() {\n\tform := huh.NewForm(\n\t\thuh.NewGroup(\n\t\t\thuh.NewNote().\n\t\t\t\tTitle(\"Welcome!\").\n\t\t\t\tDescription(\"This is an accessible form example!\"),\n\t\t\thuh.NewInput().\n\t\t\t\tValidate(validate).\n\t\t\t\tTitle(\"Name:\"),\n\t\t\thuh.NewInput().\n\t\t\t\tEchoMode(huh.EchoModePassword).\n\t\t\t\tValidate(validate).\n\t\t\t\tTitle(\"Password:\"),\n\t\t\thuh.NewMultiSelect[string]().\n\t\t\t\tOptions(huh.NewOptions(\n\t\t\t\t\t\"Red\",\n\t\t\t\t\t\"Green\",\n\t\t\t\t\t\"Yellow\",\n\t\t\t\t)...).\n\t\t\t\tLimit(2).\n\t\t\t\tTitle(\"Choose some colors:\"),\n\t\t\thuh.NewSelect[string]().\n\t\t\t\tOptions(huh.NewOptions(\n\t\t\t\t\t\"Red\",\n\t\t\t\t\t\"Green\",\n\t\t\t\t\t\"Yellow\",\n\t\t\t\t)...).\n\t\t\t\tTitle(\"Choose the best color:\"),\n\t\t\thuh.NewFilePicker().\n\t\t\t\tTitle(\"Which file?\"),\n\t\t\thuh.NewConfirm().\n\t\t\t\tTitle(\"Send something?\"),\n\t\t),\n\t).WithAccessible(true)\n\n\terr := form.Run()\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n"
  },
  {
    "path": "examples/bubbletea/demo.tape",
    "content": "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\nSleep 2s\n\nDown Sleep 1s\nEnter Sleep 1s\nDown Sleep 1s\nEnter Sleep 1s\nEnter Sleep 1.5s\nLeft Sleep 2s\nEnter Sleep 3s\n"
  },
  {
    "path": "examples/bubbletea/main.go",
    "content": "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\"charm.land/lipgloss/v2\"\n)\n\nconst maxWidth = 80\n\ntype Styles struct {\n\tBase,\n\tHeaderText,\n\tStatus,\n\tStatusHeader,\n\tHighlight,\n\tErrorHeaderText,\n\tHelp lipgloss.Style\n\n\tRed, Indigo, Green color.Color\n}\n\nfunc NewStyles(hasDarkBg bool) *Styles {\n\tvar (\n\t\ts         = Styles{}\n\t\tlightDark = lipgloss.LightDark(hasDarkBg)\n\t)\n\n\ts.Red = lightDark(lipgloss.Color(\"#FE5F86\"), lipgloss.Color(\"#FE5F86\"))\n\ts.Indigo = lightDark(lipgloss.Color(\"#5A56E0\"), lipgloss.Color(\"#7571F9\"))\n\ts.Green = lightDark(lipgloss.Color(\"#02BA84\"), lipgloss.Color(\"#02BF87\"))\n\ts.Base = lipgloss.NewStyle().\n\t\tPadding(1, 4, 0, 1)\n\ts.HeaderText = lipgloss.NewStyle().\n\t\tForeground(s.Indigo).\n\t\tBold(true).\n\t\tPadding(0, 1, 0, 2)\n\ts.Status = lipgloss.NewStyle().\n\t\tBorder(lipgloss.RoundedBorder()).\n\t\tBorderForeground(s.Indigo).\n\t\tPaddingLeft(1).\n\t\tMarginTop(1)\n\ts.StatusHeader = lipgloss.NewStyle().\n\t\tForeground(s.Green).\n\t\tBold(true)\n\ts.Highlight = lipgloss.NewStyle().\n\t\tForeground(lipgloss.Color(\"212\"))\n\ts.ErrorHeaderText = s.HeaderText.\n\t\tForeground(s.Red)\n\ts.Help = lipgloss.NewStyle().\n\t\tForeground(lipgloss.Color(\"240\"))\n\treturn &s\n}\n\ntype state int\n\nconst (\n\tstatusNormal state = iota\n\tstateDone\n)\n\ntype Model struct {\n\tstate     state\n\tstyles    func(bool) *Styles\n\tform      *huh.Form\n\thasDarkBg bool\n\twidth     int\n}\n\nfunc NewModel() Model {\n\tm := Model{\n\t\twidth:  maxWidth,\n\t\tstyles: NewStyles,\n\t}\n\n\tm.form = huh.NewForm(\n\t\thuh.NewGroup(\n\t\t\thuh.NewSelect[string]().\n\t\t\t\tKey(\"class\").\n\t\t\t\tOptions(huh.NewOptions(\"Warrior\", \"Mage\", \"Rogue\")...).\n\t\t\t\tTitle(\"Choose your class\").\n\t\t\t\tDescription(\"This will determine your department\"),\n\n\t\t\thuh.NewSelect[string]().\n\t\t\t\tKey(\"level\").\n\t\t\t\tOptions(huh.NewOptions(\"1\", \"20\", \"9999\")...).\n\t\t\t\tTitle(\"Choose your level\").\n\t\t\t\tDescription(\"This will determine your benefits package\"),\n\n\t\t\thuh.NewConfirm().\n\t\t\t\tKey(\"done\").\n\t\t\t\tTitle(\"All done?\").\n\t\t\t\tValidate(func(v bool) error {\n\t\t\t\t\tif !v {\n\t\t\t\t\t\treturn fmt.Errorf(\"Welp, finish up then\")\n\t\t\t\t\t}\n\t\t\t\t\treturn nil\n\t\t\t\t}).\n\t\t\t\tAffirmative(\"Yep\").\n\t\t\t\tNegative(\"Wait, no\"),\n\t\t),\n\t).\n\t\tWithWidth(45).\n\t\tWithShowHelp(false).\n\t\tWithShowErrors(false)\n\treturn m\n}\n\nfunc (m Model) Init() tea.Cmd {\n\treturn m.form.Init()\n}\n\nfunc min(x, y int) int {\n\tif x > y {\n\t\treturn y\n\t}\n\treturn x\n}\n\nfunc (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {\n\tstyles := m.styles(m.hasDarkBg)\n\tswitch msg := msg.(type) {\n\tcase tea.BackgroundColorMsg:\n\t\tm.hasDarkBg = msg.IsDark()\n\tcase tea.WindowSizeMsg:\n\t\tm.width = min(msg.Width, maxWidth) - styles.Base.GetHorizontalFrameSize()\n\tcase tea.KeyPressMsg:\n\t\tswitch msg.String() {\n\t\tcase \"ctrl+c\":\n\t\t\treturn m, tea.Interrupt\n\t\tcase \"esc\", \"q\":\n\t\t\treturn m, tea.Quit\n\t\t}\n\t}\n\n\tvar cmds []tea.Cmd\n\n\t// Process the form\n\tform, cmd := m.form.Update(msg)\n\tif f, ok := form.(*huh.Form); ok {\n\t\tm.form = f\n\t\tcmds = append(cmds, cmd)\n\t}\n\n\tif m.form.State == huh.StateCompleted {\n\t\t// Quit when the form is done.\n\t\tcmds = append(cmds, tea.Quit)\n\t}\n\n\treturn m, tea.Batch(cmds...)\n}\n\nfunc (m Model) View() tea.View {\n\ts := m.styles(m.hasDarkBg)\n\n\tswitch m.form.State {\n\tcase huh.StateCompleted:\n\t\ttitle, role := m.getRole()\n\t\ttitle = s.Highlight.Render(title)\n\t\tvar b strings.Builder\n\t\tfmt.Fprintf(&b, \"Congratulations, you’re Charm’s newest\\n%s!\\n\\n\", title)\n\t\tfmt.Fprintf(&b, \"Your job description is as follows:\\n\\n%s\\n\\nPlease proceed to HR immediately.\", role)\n\t\treturn tea.NewView(s.Status.Margin(0, 1).Padding(1, 2).Width(48).Render(b.String()) + \"\\n\\n\")\n\tdefault:\n\n\t\tvar class string\n\t\tif m.form.GetString(\"class\") != \"\" {\n\t\t\tclass = \"Class: \" + m.form.GetString(\"class\")\n\t\t}\n\n\t\t// Form (left side)\n\t\tv := strings.TrimSuffix(m.form.View(), \"\\n\\n\")\n\t\tform := lipgloss.NewStyle().Margin(1, 0).Render(v)\n\n\t\t// Status (right side)\n\t\tvar status string\n\t\t{\n\t\t\tvar (\n\t\t\t\tbuildInfo      = \"(None)\"\n\t\t\t\trole           string\n\t\t\t\tjobDescription string\n\t\t\t\tlevel          string\n\t\t\t)\n\n\t\t\tif m.form.GetString(\"level\") != \"\" {\n\t\t\t\tlevel = \"Level: \" + m.form.GetString(\"level\")\n\t\t\t\trole, jobDescription = m.getRole()\n\t\t\t\trole = \"\\n\\n\" + s.StatusHeader.Render(\"Projected Role\") + \"\\n\" + role\n\t\t\t\tjobDescription = \"\\n\\n\" + s.StatusHeader.Render(\"Duties\") + \"\\n\" + jobDescription\n\t\t\t}\n\t\t\tif m.form.GetString(\"class\") != \"\" {\n\t\t\t\tbuildInfo = fmt.Sprintf(\"%s\\n%s\", class, level)\n\t\t\t}\n\n\t\t\tconst statusWidth = 28\n\t\t\tstatusMarginLeft := m.width - statusWidth - lipgloss.Width(form) - s.Status.GetMarginRight()\n\t\t\tstatus = s.Status.\n\t\t\t\tHeight(lipgloss.Height(form)).\n\t\t\t\tWidth(statusWidth).\n\t\t\t\tMarginLeft(statusMarginLeft).\n\t\t\t\tRender(s.StatusHeader.Render(\"Current Build\") + \"\\n\" +\n\t\t\t\t\tbuildInfo +\n\t\t\t\t\trole +\n\t\t\t\t\tjobDescription)\n\t\t}\n\n\t\terrors := m.form.Errors()\n\t\theader := m.appBoundaryView(\"Charm Employment Application\")\n\t\tif len(errors) > 0 {\n\t\t\theader = m.appErrorBoundaryView(m.errorView())\n\t\t}\n\t\tbody := lipgloss.JoinHorizontal(lipgloss.Left, form, status)\n\n\t\tfooter := m.appBoundaryView(m.form.Help().ShortHelpView(m.form.KeyBinds()))\n\t\tif len(errors) > 0 {\n\t\t\tfooter = m.appErrorBoundaryView(\"\")\n\t\t}\n\n\t\treturn tea.NewView(s.Base.Render(header + \"\\n\" + body + \"\\n\\n\" + footer))\n\t}\n}\n\nfunc (m Model) errorView() string {\n\tvar s string\n\tfor _, err := range m.form.Errors() {\n\t\ts += err.Error()\n\t}\n\treturn s\n}\n\nfunc (m Model) appBoundaryView(text string) string {\n\ts := m.styles(m.hasDarkBg)\n\treturn lipgloss.PlaceHorizontal(\n\t\tm.width,\n\t\tlipgloss.Left,\n\t\ts.HeaderText.Render(text),\n\t\tlipgloss.WithWhitespaceChars(\"/\"),\n\t\tlipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Foreground(s.Indigo)),\n\t)\n}\n\nfunc (m Model) appErrorBoundaryView(text string) string {\n\ts := m.styles(m.hasDarkBg)\n\treturn lipgloss.PlaceHorizontal(\n\t\tm.width,\n\t\tlipgloss.Left,\n\t\ts.ErrorHeaderText.Render(text),\n\t\tlipgloss.WithWhitespaceChars(\"/\"),\n\t\tlipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Foreground(s.Red)),\n\t)\n}\n\nfunc (m Model) getRole() (string, string) {\n\tlevel := m.form.GetString(\"level\")\n\tswitch m.form.GetString(\"class\") {\n\tcase \"Warrior\":\n\t\tswitch level {\n\t\tcase \"1\":\n\t\t\treturn \"Tank Intern\", \"Assists with tank-related activities. Paid position.\"\n\t\tcase \"9999\":\n\t\t\treturn \"Tank Manager\", \"Manages tanks and tank-related activities.\"\n\t\tdefault:\n\t\t\treturn \"Tank\", \"General tank. Does damage, takes damage. Responsible for tanking.\"\n\t\t}\n\tcase \"Mage\":\n\t\tswitch level {\n\t\tcase \"1\":\n\t\t\treturn \"DPS Associate\", \"Finds DPS deals and passes them on to DPS Manager.\"\n\t\tcase \"9999\":\n\t\t\treturn \"DPS Operating Officer\", \"Oversees all DPS activities.\"\n\t\tdefault:\n\t\t\treturn \"DPS\", \"Does damage and ideally does not take damage. Logs hours in JIRA.\"\n\t\t}\n\tcase \"Rogue\":\n\t\tswitch level {\n\t\tcase \"1\":\n\t\t\treturn \"Stealth Junior Designer\", \"Designs rogue-like activities. Reports to Stealth Lead.\"\n\t\tcase \"9999\":\n\t\t\treturn \"Stealth Lead\", \"Lead designer for all things stealth. Some travel required.\"\n\t\tdefault:\n\t\t\treturn \"Sneaky Person\", \"Sneaks around and does sneaky things. Reports to Stealth Lead.\"\n\t\t}\n\tdefault:\n\t\treturn \"\", \"\"\n\t}\n}\n\nfunc main() {\n\t_, err := tea.NewProgram(NewModel()).Run()\n\tif err != nil {\n\t\tfmt.Println(\"Oh no:\", err)\n\t\tos.Exit(1)\n\t}\n}\n"
  },
  {
    "path": "examples/bubbletea-options/main.go",
    "content": "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\tform := huh.NewForm(\n\t\thuh.NewGroup(huh.NewInput().Description(\"What should we call you?\").Value(&name)),\n\t).WithViewHook(func(v tea.View) tea.View {\n\t\tv.AltScreen = true\n\t\treturn v\n\t})\n\n\terr := form.Run()\n\tif err != nil {\n\t\tfmt.Println(\"error:\", err)\n\t}\n\n\tfmt.Println(\"Welcome, \" + name + \"!\")\n}\n"
  },
  {
    "path": "examples/burger/demo.tape",
    "content": "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 && ./burger\"\nSleep 500ms\nEnter\nSleep 500ms\n\nShow\n\nSleep 1s\nType \"n\"\nSleep 1s\nDown 2\nSleep 500ms Enter\nSleep 1s\nUp@500ms\nSleep 500ms Enter\nSleep 500ms\nDown@300ms 3\nSleep 300ms\nSpace\nSleep 750ms Enter\nSleep 500ms\nDown@300ms 2\nSleep 500ms Enter\nSleep 750ms\nDown@300ms\nSleep 500ms Enter\n\nSleep 1s\n\nType \"Hilda\"\nSleep 500ms Enter\n\nSleep 1s\nType \"Extra spicy please!\"\nSleep 500ms Tab\n\nSleep 750ms\nLeft\nSleep 750ms Enter\n\nSleep 5s\n\nHide\nType \"rm burger\" Enter\nSleep 1s\n"
  },
  {
    "path": "examples/burger/main.go",
    "content": "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/spinner\"\n\t\"charm.land/lipgloss/v2\"\n\txstrings \"github.com/charmbracelet/x/exp/strings\"\n)\n\ntype Spice int\n\nconst (\n\tMild Spice = iota + 1\n\tMedium\n\tHot\n)\n\nfunc (s Spice) String() string {\n\tswitch s {\n\tcase Mild:\n\t\treturn \"Mild \"\n\tcase Medium:\n\t\treturn \"Medium-Spicy \"\n\tcase Hot:\n\t\treturn \"Spicy-Hot \"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\ntype Order struct {\n\tBurger       Burger\n\tSide         string\n\tName         string\n\tInstructions string\n\tDiscount     bool\n}\n\ntype Burger struct {\n\tType     string\n\tToppings []string\n\tSpice    Spice\n}\n\nfunc main() {\n\tvar burger Burger\n\torder := Order{Burger: burger}\n\n\t// Should we run in accessible mode?\n\taccessible, _ := strconv.ParseBool(os.Getenv(\"ACCESSIBLE\"))\n\n\tform := huh.NewForm(\n\t\thuh.NewGroup(huh.NewNote().\n\t\t\tTitle(\"Charmburger\").\n\t\t\tDescription(\"Welcome to _Charmburger™_.\\n\\nHow may we take your order?\").\n\t\t\tNext(true).\n\t\t\tNextLabel(\"Next\"),\n\t\t),\n\n\t\t// Choose a burger.\n\t\t// We'll need to know what topping to add too.\n\t\thuh.NewGroup(\n\t\t\thuh.NewSelect[string]().\n\t\t\t\tOptions(huh.NewOptions(\"Charmburger Classic\", \"Chickwich\", \"Fishburger\", \"Charmpossible™ Burger\")...).\n\t\t\t\tTitle(\"Choose your burger\").\n\t\t\t\tDescription(\"At Charm we truly have a burger for everyone.\").\n\t\t\t\tValidate(func(t string) error {\n\t\t\t\t\tif t == \"Fishburger\" {\n\t\t\t\t\t\treturn fmt.Errorf(\"no fish today, sorry\")\n\t\t\t\t\t}\n\t\t\t\t\treturn nil\n\t\t\t\t}).\n\t\t\t\tValue(&order.Burger.Type),\n\n\t\t\thuh.NewMultiSelect[string]().\n\t\t\t\tTitle(\"Toppings\").\n\t\t\t\tDescription(\"Choose up to 4.\").\n\t\t\t\tOptions(\n\t\t\t\t\thuh.NewOption(\"Lettuce\", \"Lettuce\").Selected(true),\n\t\t\t\t\thuh.NewOption(\"Tomatoes\", \"Tomatoes\").Selected(true),\n\t\t\t\t\thuh.NewOption(\"Charm Sauce\", \"Charm Sauce\"),\n\t\t\t\t\thuh.NewOption(\"Jalapeños\", \"Jalapeños\"),\n\t\t\t\t\thuh.NewOption(\"Cheese\", \"Cheese\"),\n\t\t\t\t\thuh.NewOption(\"Vegan Cheese\", \"Vegan Cheese\"),\n\t\t\t\t\thuh.NewOption(\"Nutella\", \"Nutella\"),\n\t\t\t\t).\n\t\t\t\tValidate(func(t []string) error {\n\t\t\t\t\tif len(t) <= 0 {\n\t\t\t\t\t\treturn fmt.Errorf(\"at least one topping is required\")\n\t\t\t\t\t}\n\t\t\t\t\treturn nil\n\t\t\t\t}).\n\t\t\t\tValue(&order.Burger.Toppings).\n\t\t\t\tFilterable(true).\n\t\t\t\tLimit(4),\n\t\t),\n\n\t\t// Prompt for toppings and special instructions.\n\t\t// The customer can ask for up to 4 toppings.\n\t\thuh.NewGroup(\n\t\t\thuh.NewSelect[Spice]().\n\t\t\t\tTitle(\"Spice level\").\n\t\t\t\tOptions(\n\t\t\t\t\thuh.NewOption(\"Mild\", Mild).Selected(true),\n\t\t\t\t\thuh.NewOption(\"Medium\", Medium),\n\t\t\t\t\thuh.NewOption(\"Hot\", Hot),\n\t\t\t\t).\n\t\t\t\tValue(&order.Burger.Spice),\n\n\t\t\thuh.NewSelect[string]().\n\t\t\t\tOptions(huh.NewOptions(\"Fries\", \"Disco Fries\", \"R&B Fries\", \"Carrots\")...).\n\t\t\t\tValue(&order.Side).\n\t\t\t\tTitle(\"Sides\").\n\t\t\t\tDescription(\"You get one free side with this order.\"),\n\t\t),\n\n\t\t// Gather final details for the order.\n\t\thuh.NewGroup(\n\t\t\thuh.NewInput().\n\t\t\t\tValue(&order.Name).\n\t\t\t\tTitle(\"What's your name?\").\n\t\t\t\tPlaceholder(\"Margaret Thatcher\").\n\t\t\t\tValidate(func(s string) error {\n\t\t\t\t\tif s == \"Frank\" {\n\t\t\t\t\t\treturn errors.New(\"no franks, sorry\")\n\t\t\t\t\t}\n\t\t\t\t\treturn nil\n\t\t\t\t}).\n\t\t\t\tDescription(\"For when your order is ready.\"),\n\n\t\t\thuh.NewText().\n\t\t\t\tValue(&order.Instructions).\n\t\t\t\tPlaceholder(\"Just put it in the mailbox please\").\n\t\t\t\tTitle(\"Special Instructions\").\n\t\t\t\tDescription(\"Anything we should know?\").\n\t\t\t\tCharLimit(400).\n\t\t\t\tLines(5),\n\n\t\t\thuh.NewConfirm().\n\t\t\t\tTitle(\"Would you like 15% off?\").\n\t\t\t\tValue(&order.Discount).\n\t\t\t\tAffirmative(\"Yes!\").\n\t\t\t\tNegative(\"No.\"),\n\t\t),\n\t).WithAccessible(accessible)\n\n\terr := form.Run()\n\tif err != nil {\n\t\tfmt.Println(\"Uh oh:\", err)\n\t\tos.Exit(1)\n\t}\n\n\tprepareBurger := func() {\n\t\ttime.Sleep(2 * time.Second)\n\t}\n\n\t_ = spinner.New().Title(\"Preparing your burger...\").WithAccessible(accessible).Action(prepareBurger).Run()\n\n\t// Print order summary.\n\t{\n\t\tvar sb strings.Builder\n\t\tkeyword := func(s string) string {\n\t\t\treturn lipgloss.NewStyle().Foreground(lipgloss.Color(\"212\")).Render(s)\n\t\t}\n\t\tfmt.Fprintf(&sb,\n\t\t\t\"%s\\n\\nOne %s%s, topped with %s with %s on the side.\",\n\t\t\tlipgloss.NewStyle().Bold(true).Render(\"BURGER RECEIPT\"),\n\t\t\tkeyword(order.Burger.Spice.String()),\n\t\t\tkeyword(order.Burger.Type),\n\t\t\tkeyword(xstrings.EnglishJoin(order.Burger.Toppings, true)),\n\t\t\tkeyword(order.Side),\n\t\t)\n\n\t\tname := order.Name\n\t\tif name != \"\" {\n\t\t\tname = \", \" + name\n\t\t}\n\t\tfmt.Fprintf(&sb, \"\\n\\nThanks for your order%s!\", name)\n\n\t\tif order.Discount {\n\t\t\tfmt.Fprint(&sb, \"\\n\\nEnjoy 15% off.\")\n\t\t}\n\n\t\tfmt.Println(\n\t\t\tlipgloss.NewStyle().\n\t\t\t\tWidth(40).\n\t\t\t\tBorderStyle(lipgloss.RoundedBorder()).\n\t\t\t\tBorderForeground(lipgloss.Color(\"63\")).\n\t\t\t\tPadding(1, 2).\n\t\t\t\tRender(sb.String()),\n\t\t)\n\t}\n}\n"
  },
  {
    "path": "examples/conditional/main.go",
    "content": "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\tvegetables\n\tdrinks\n)\n\nfunc (c consumable) String() string {\n\treturn [...]string{\"fruit\", \"vegetable\", \"drink\"}[c]\n}\n\nfunc main() {\n\tvar category consumable\n\ttype opts []huh.Option[string]\n\n\tvar choice string\n\n\t// Then ask for a specific food item based on the previous answer.\n\terr := huh.NewForm(\n\t\thuh.NewGroup(\n\t\t\thuh.NewSelect[consumable]().\n\t\t\t\tTitle(\"What are you in the mood for?\").\n\t\t\t\tValue(&category).\n\t\t\t\tOptions(\n\t\t\t\t\thuh.NewOption(\"Some fruit\", fruits),\n\t\t\t\t\thuh.NewOption(\"A vegetable\", vegetables),\n\t\t\t\t\thuh.NewOption(\"A drink\", drinks),\n\t\t\t\t),\n\n\t\t\thuh.NewSelect[string]().\n\t\t\t\tValue(&choice).\n\t\t\t\tHeight(7).\n\t\t\t\tTitleFunc(func() string {\n\t\t\t\t\treturn fmt.Sprintf(\"Okay, what kind of %s are you in the mood for?\", category)\n\t\t\t\t}, &category).\n\t\t\t\tOptionsFunc(func() []huh.Option[string] {\n\t\t\t\t\tswitch category {\n\t\t\t\t\tcase fruits:\n\t\t\t\t\t\treturn []huh.Option[string]{\n\t\t\t\t\t\t\thuh.NewOption(\"Tangerine\", \"tangerine\"),\n\t\t\t\t\t\t\thuh.NewOption(\"Canteloupe\", \"canteloupe\"),\n\t\t\t\t\t\t\thuh.NewOption(\"Pomelo\", \"pomelo\"),\n\t\t\t\t\t\t\thuh.NewOption(\"Grapefruit\", \"grapefruit\"),\n\t\t\t\t\t\t}\n\t\t\t\t\tcase vegetables:\n\t\t\t\t\t\treturn []huh.Option[string]{\n\t\t\t\t\t\t\thuh.NewOption(\"Carrot\", \"carrot\"),\n\t\t\t\t\t\t\thuh.NewOption(\"Jicama\", \"jicama\"),\n\t\t\t\t\t\t\thuh.NewOption(\"Kohlrabi\", \"kohlrabi\"),\n\t\t\t\t\t\t\thuh.NewOption(\"Fennel\", \"fennel\"),\n\t\t\t\t\t\t\thuh.NewOption(\"Ginger\", \"ginger\"),\n\t\t\t\t\t\t}\n\t\t\t\t\tdefault:\n\t\t\t\t\t\treturn []huh.Option[string]{\n\t\t\t\t\t\t\thuh.NewOption(\"Coffee\", \"coffee\"),\n\t\t\t\t\t\t\thuh.NewOption(\"Tea\", \"tea\"),\n\t\t\t\t\t\t\thuh.NewOption(\"Bubble Tea\", \"bubble tea\"),\n\t\t\t\t\t\t\thuh.NewOption(\"Agua Fresca\", \"agua-fresca\"),\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}, &category),\n\t\t),\n\t).Run()\n\tif err != nil {\n\t\tfmt.Println(\"Trouble in food paradise:\", err)\n\t\tos.Exit(1)\n\t}\n\n\tfmt.Printf(\"One %s coming right up!\\n\", choice)\n}\n"
  },
  {
    "path": "examples/dynamic/demo.tape",
    "content": "Output dynamic.gif\n\nSet Shell \"bash\"\nSet FontSize 28\nSet Width 1000\nSet Height 700\n\nHide\n  Type \"clear && go build -o dynamic ./dynamic-country\"\n  Enter\n  Sleep 1s\nShow\n\nSleep 1s\nType \"./dynamic\" Sleep 500ms  Enter\n\nSleep 3.5s\nDown\nSleep 2.5s\nDown\nSleep 2.5s\nEnter\nSleep 1s\nDown@150ms 12\nUp@150ms 2\n\nSleep 1s\n\nEnter\n\nSleep 3s\n\nHide\n  Type \"rm dynamic\"\nShow\n"
  },
  {
    "path": "examples/dynamic/dynamic-all/main.go",
    "content": "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.NewForm(\n\t\thuh.NewGroup(\n\t\t\thuh.NewInput().Value(&value).Title(\"Dynamic\").Description(\"Dynamic\"),\n\t\t\thuh.NewNote().\n\t\t\t\tTitleFunc(func() string { return value }, &value).\n\t\t\t\tDescriptionFunc(func() string { return value }, &value),\n\t\t\thuh.NewSelect[string]().\n\t\t\t\tHeight(7).\n\t\t\t\tTitleFunc(func() string { return value }, &value).\n\t\t\t\tDescriptionFunc(func() string { return value }, &value).\n\t\t\t\tOptionsFunc(func() []huh.Option[string] {\n\t\t\t\t\tvar options []huh.Option[string]\n\t\t\t\t\tfor i := 1; i < 6; i++ {\n\t\t\t\t\t\toptions = append(options, huh.NewOption(value+\" \"+strconv.Itoa(i), value+strconv.Itoa(i)))\n\t\t\t\t\t}\n\t\t\t\t\treturn options\n\t\t\t\t}, &value),\n\t\t\thuh.NewMultiSelect[string]().\n\t\t\t\tHeight(7).\n\t\t\t\tTitleFunc(func() string { return value }, &value).\n\t\t\t\tDescriptionFunc(func() string { return value }, &value).\n\t\t\t\tOptionsFunc(func() []huh.Option[string] {\n\t\t\t\t\tvar options []huh.Option[string]\n\t\t\t\t\tfor i := 1; i < 6; i++ {\n\t\t\t\t\t\toptions = append(options, huh.NewOption(value+\" \"+strconv.Itoa(i), value+strconv.Itoa(i)))\n\t\t\t\t\t}\n\t\t\t\t\treturn options\n\t\t\t\t}, &value),\n\t\t\thuh.NewConfirm().\n\t\t\t\tTitleFunc(func() string { return value }, &value).\n\t\t\t\tDescriptionFunc(func() string { return value }, &value),\n\t\t\thuh.NewText().\n\t\t\t\tTitleFunc(func() string { return value }, &value).\n\t\t\t\tDescriptionFunc(func() string { return value }, &value).\n\t\t\t\tPlaceholderFunc(func() string { return value }, &value),\n\t\t),\n\t)\n\terr := f.Run()\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n"
  },
  {
    "path": "examples/dynamic/dynamic-bubbletea/main.go",
    "content": "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\"charm.land/lipgloss/v2\"\n)\n\nconst maxWidth = 80\n\ntype Styles struct {\n\tBase,\n\tHeaderText,\n\tStatus,\n\tStatusHeader,\n\tHighlight,\n\tErrorHeaderText,\n\tHelp lipgloss.Style\n\n\tRed, Indigo, Green color.Color\n}\n\nfunc NewStyles(hasDarkBg bool) *Styles {\n\tvar (\n\t\ts         = Styles{}\n\t\tlightDark = lipgloss.LightDark(hasDarkBg)\n\t)\n\n\ts.Red = lightDark(lipgloss.Color(\"#FE5F86\"), lipgloss.Color(\"#FE5F86\"))\n\ts.Indigo = lightDark(lipgloss.Color(\"#5A56E0\"), lipgloss.Color(\"#7571F9\"))\n\ts.Green = lightDark(lipgloss.Color(\"#02BA84\"), lipgloss.Color(\"#02BF87\"))\n\ts.Base = lipgloss.NewStyle().\n\t\tPadding(1, 4, 0, 1)\n\ts.HeaderText = lipgloss.NewStyle().\n\t\tForeground(s.Indigo).\n\t\tBold(true).\n\t\tPadding(0, 1, 0, 2)\n\ts.Status = lipgloss.NewStyle().\n\t\tBorder(lipgloss.RoundedBorder()).\n\t\tBorderForeground(s.Indigo).\n\t\tPaddingLeft(1).\n\t\tMarginTop(1)\n\ts.StatusHeader = lipgloss.NewStyle().\n\t\tForeground(s.Green).\n\t\tBold(true)\n\ts.Highlight = lipgloss.NewStyle().\n\t\tForeground(lipgloss.Color(\"212\"))\n\ts.ErrorHeaderText = s.HeaderText.\n\t\tForeground(s.Red)\n\ts.Help = lipgloss.NewStyle().\n\t\tForeground(lipgloss.Color(\"240\"))\n\treturn &s\n}\n\ntype state int\n\nconst (\n\tstatusNormal state = iota\n\tstateDone\n)\n\ntype Model struct {\n\tstate     state\n\tstyles    func(bool) *Styles\n\thasDarkBg bool\n\tform      *huh.Form\n\twidth     int\n}\n\nfunc NewModel() Model {\n\tm := Model{width: maxWidth, styles: NewStyles}\n\n\tvar class string\n\n\tm.form = huh.NewForm(\n\t\thuh.NewGroup(\n\t\t\thuh.NewSelect[string]().\n\t\t\t\tKey(\"class\").\n\t\t\t\tValue(&class).\n\t\t\t\tOptions(huh.NewOptions(\"Warrior\", \"Mage\", \"Rogue\")...).\n\t\t\t\tTitle(\"Choose your class\").\n\t\t\t\tDescription(\"This will determine your department\"),\n\n\t\t\thuh.NewSelect[string]().\n\t\t\t\tKey(\"level\").\n\t\t\t\tOptionsFunc(func() []huh.Option[string] {\n\t\t\t\t\tswitch class {\n\t\t\t\t\tcase \"Warrior\":\n\t\t\t\t\t\treturn huh.NewOptions(\"1\", \"20\", \"9999\")\n\t\t\t\t\tcase \"Mage\":\n\t\t\t\t\t\treturn huh.NewOptions(\"10\", \"100\", \"1000\")\n\t\t\t\t\t}\n\t\t\t\t\treturn huh.NewOptions(\"1\", \"20\", \"9999\")\n\t\t\t\t}, &class).\n\t\t\t\tTitle(\"Choose your level\").\n\t\t\t\tDescription(\"This will determine your benefits package\"),\n\n\t\t\thuh.NewConfirm().\n\t\t\t\tKey(\"done\").\n\t\t\t\tTitle(\"All done?\").\n\t\t\t\tValidate(func(v bool) error {\n\t\t\t\t\tif !v {\n\t\t\t\t\t\treturn fmt.Errorf(\"Welp, finish up then\")\n\t\t\t\t\t}\n\t\t\t\t\treturn nil\n\t\t\t\t}).\n\t\t\t\tAffirmative(\"Yep\").\n\t\t\t\tNegative(\"Wait, no\"),\n\t\t),\n\t).\n\t\tWithWidth(45).\n\t\tWithShowHelp(false).\n\t\tWithShowErrors(false)\n\treturn m\n}\n\nfunc (m Model) Init() tea.Cmd {\n\treturn m.form.Init()\n}\n\nfunc min(x, y int) int {\n\tif x > y {\n\t\treturn y\n\t}\n\treturn x\n}\n\nfunc (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {\n\tswitch msg := msg.(type) {\n\tcase tea.BackgroundColorMsg:\n\t\tm.hasDarkBg = msg.IsDark()\n\tcase tea.WindowSizeMsg:\n\t\ts := m.styles(m.hasDarkBg)\n\t\tm.width = min(msg.Width, maxWidth) - s.Base.GetHorizontalFrameSize()\n\tcase tea.KeyPressMsg:\n\t\tswitch msg.String() {\n\t\tcase \"esc\", \"ctrl+c\", \"q\":\n\t\t\treturn m, tea.Quit\n\t\t}\n\t}\n\n\tvar cmds []tea.Cmd\n\n\t// Process the form\n\tform, cmd := m.form.Update(msg)\n\tif f, ok := form.(*huh.Form); ok {\n\t\tm.form = f\n\t\tcmds = append(cmds, cmd)\n\t}\n\n\tif m.form.State == huh.StateCompleted {\n\t\t// Quit when the form is done.\n\t\tcmds = append(cmds, tea.Quit)\n\t}\n\n\treturn m, tea.Batch(cmds...)\n}\n\nfunc (m Model) View() tea.View {\n\ts := m.styles(m.hasDarkBg)\n\n\tswitch m.form.State {\n\tcase huh.StateCompleted:\n\t\ttitle, role := m.getRole()\n\t\ttitle = s.Highlight.Render(title)\n\t\tvar b strings.Builder\n\t\tfmt.Fprintf(&b, \"Congratulations, you’re Charm’s newest\\n%s!\\n\\n\", title)\n\t\tfmt.Fprintf(&b, \"Your job description is as follows:\\n\\n%s\\n\\nPlease proceed to HR immediately.\", role)\n\t\treturn tea.NewView(s.Status.Margin(0, 1).Padding(1, 2).Width(48).Render(b.String()) + \"\\n\\n\")\n\tdefault:\n\n\t\tvar class string\n\t\tif m.form.GetString(\"class\") != \"\" {\n\t\t\tclass = \"Class: \" + m.form.GetString(\"class\")\n\t\t}\n\n\t\t// Form (left side)\n\t\tv := strings.TrimSuffix(m.form.View(), \"\\n\\n\")\n\t\tform := lipgloss.NewStyle().Margin(1, 0).Render(v)\n\n\t\t// Status (right side)\n\t\tvar status string\n\t\t{\n\t\t\tvar (\n\t\t\t\tbuildInfo      = \"(None)\"\n\t\t\t\trole           string\n\t\t\t\tjobDescription string\n\t\t\t\tlevel          string\n\t\t\t)\n\n\t\t\tif m.form.GetString(\"level\") != \"\" {\n\t\t\t\tlevel = \"Level: \" + m.form.GetString(\"level\")\n\t\t\t\trole, jobDescription = m.getRole()\n\t\t\t\trole = \"\\n\\n\" + s.StatusHeader.Render(\"Projected Role\") + \"\\n\" + role\n\t\t\t\tjobDescription = \"\\n\\n\" + s.StatusHeader.Render(\"Duties\") + \"\\n\" + jobDescription\n\t\t\t}\n\t\t\tif m.form.GetString(\"class\") != \"\" {\n\t\t\t\tbuildInfo = fmt.Sprintf(\"%s\\n%s\", class, level)\n\t\t\t}\n\n\t\t\tconst statusWidth = 28\n\t\t\tstatusMarginLeft := m.width - statusWidth - lipgloss.Width(form) - s.Status.GetMarginRight()\n\t\t\tstatus = s.Status.\n\t\t\t\tHeight(lipgloss.Height(form)).\n\t\t\t\tWidth(statusWidth).\n\t\t\t\tMarginLeft(statusMarginLeft).\n\t\t\t\tRender(s.StatusHeader.Render(\"Current Build\") + \"\\n\" +\n\t\t\t\t\tbuildInfo +\n\t\t\t\t\trole +\n\t\t\t\t\tjobDescription)\n\t\t}\n\n\t\terrors := m.form.Errors()\n\t\theader := m.appBoundaryView(\"Charm Employment Application\")\n\t\tif len(errors) > 0 {\n\t\t\theader = m.appErrorBoundaryView(m.errorView())\n\t\t}\n\t\tbody := lipgloss.JoinHorizontal(lipgloss.Left, form, status)\n\n\t\tfooter := m.appBoundaryView(m.form.Help().ShortHelpView(m.form.KeyBinds()))\n\t\tif len(errors) > 0 {\n\t\t\tfooter = m.appErrorBoundaryView(\"\")\n\t\t}\n\n\t\treturn tea.NewView(s.Base.Render(header + \"\\n\" + body + \"\\n\\n\" + footer))\n\t}\n}\n\nfunc (m Model) errorView() string {\n\tvar s string\n\tfor _, err := range m.form.Errors() {\n\t\ts += err.Error()\n\t}\n\treturn s\n}\n\nfunc (m Model) appBoundaryView(text string) string {\n\ts := m.styles(m.hasDarkBg)\n\treturn lipgloss.PlaceHorizontal(\n\t\tm.width,\n\t\tlipgloss.Left,\n\t\ts.HeaderText.Render(text),\n\t\tlipgloss.WithWhitespaceChars(\"/\"),\n\t\tlipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Foreground(s.Indigo)),\n\t)\n}\n\nfunc (m Model) appErrorBoundaryView(text string) string {\n\ts := m.styles(m.hasDarkBg)\n\treturn lipgloss.PlaceHorizontal(\n\t\tm.width,\n\t\tlipgloss.Left,\n\t\ts.ErrorHeaderText.Render(text),\n\t\tlipgloss.WithWhitespaceChars(\"/\"),\n\t\tlipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Foreground(s.Red)),\n\t)\n}\n\nfunc (m Model) getRole() (string, string) {\n\tlevel := m.form.GetString(\"level\")\n\tswitch m.form.GetString(\"class\") {\n\tcase \"Warrior\":\n\t\tswitch level {\n\t\tcase \"1\":\n\t\t\treturn \"Tank Intern\", \"Assists with tank-related activities. Paid position.\"\n\t\tcase \"9999\":\n\t\t\treturn \"Tank Manager\", \"Manages tanks and tank-related activities.\"\n\t\tdefault:\n\t\t\treturn \"Tank\", \"General tank. Does damage, takes damage. Responsible for tanking.\"\n\t\t}\n\tcase \"Mage\":\n\t\tswitch level {\n\t\tcase \"1\":\n\t\t\treturn \"DPS Associate\", \"Finds DPS deals and passes them on to DPS Manager.\"\n\t\tcase \"9999\":\n\t\t\treturn \"DPS Operating Officer\", \"Oversees all DPS activities.\"\n\t\tdefault:\n\t\t\treturn \"DPS\", \"Does damage and ideally does not take damage. Logs hours in JIRA.\"\n\t\t}\n\tcase \"Rogue\":\n\t\tswitch level {\n\t\tcase \"1\":\n\t\t\treturn \"Stealth Junior Designer\", \"Designs rougue-like activities. Reports to Stealth Lead.\"\n\t\tcase \"9999\":\n\t\t\treturn \"Stealth Lead\", \"Lead designer for all things stealth. Some travel required.\"\n\t\tdefault:\n\t\t\treturn \"Sneaky Person\", \"Sneaks around and does sneaky things. Reports to Stealth Lead.\"\n\t\t}\n\tdefault:\n\t\treturn \"\", \"\"\n\t}\n}\n\nfunc main() {\n\t_, err := tea.NewProgram(NewModel()).Run()\n\tif err != nil {\n\t\tfmt.Println(\"Oh no:\", err)\n\t\tos.Exit(1)\n\t}\n}\n"
  },
  {
    "path": "examples/dynamic/dynamic-count/main.go",
    "content": "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\tdefaultValue := 10\n\tvar chosen int\n\n\tf := huh.NewForm(\n\t\thuh.NewGroup(\n\t\t\thuh.NewInput().\n\t\t\t\tValue(&value).\n\t\t\t\tTitle(\"Max\").\n\t\t\t\tPlaceholder(strconv.Itoa(defaultValue)).\n\t\t\t\tValidate(func(s string) error {\n\t\t\t\t\tv, err := strconv.Atoi(value)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn errors.New(\"max should be a number\")\n\t\t\t\t\t}\n\t\t\t\t\tif v <= 0 {\n\t\t\t\t\t\treturn errors.New(\"maximum must be positive\")\n\t\t\t\t\t}\n\t\t\t\t\treturn nil\n\t\t\t\t}).\n\t\t\t\tDescription(\"Select a maximum\"),\n\n\t\t\thuh.NewSelect[int]().\n\t\t\t\tValue(&chosen).\n\t\t\t\tTitle(\"Pick a number\").\n\t\t\t\tDescriptionFunc(func() string {\n\t\t\t\t\tv, err := strconv.Atoi(value)\n\t\t\t\t\tif err != nil || v <= 0 {\n\t\t\t\t\t\tv = defaultValue\n\t\t\t\t\t}\n\t\t\t\t\treturn \"Between 1 and \" + strconv.Itoa(v)\n\t\t\t\t}, &value).\n\t\t\t\tOptionsFunc(func() []huh.Option[int] {\n\t\t\t\t\tvar options []huh.Option[int]\n\t\t\t\t\tv, err := strconv.Atoi(value)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tv = defaultValue\n\t\t\t\t\t}\n\t\t\t\t\tfor i := range v {\n\t\t\t\t\t\toptions = append(options, huh.NewOption(strconv.Itoa(i+1), i+1))\n\t\t\t\t\t}\n\t\t\t\t\treturn options\n\t\t\t\t}, &value),\n\t\t),\n\t)\n\terr := f.Run()\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tfmt.Println(chosen)\n}\n"
  },
  {
    "path": "examples/dynamic/dynamic-country/main.go",
    "content": "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.SetReportTimestamp(false)\n\n\tvar (\n\t\tcountry string\n\t\tstate   string\n\t)\n\n\tform := huh.NewForm(\n\t\thuh.NewGroup(\n\t\t\thuh.NewSelect[string]().\n\t\t\t\tOptions(huh.NewOptions(\"United States\", \"Canada\", \"Mexico\")...).\n\t\t\t\tValue(&country).\n\t\t\t\tTitle(\"Country\").\n\t\t\t\tHeight(5),\n\t\t\thuh.NewSelect[string]().\n\t\t\t\tValue(&state).\n\t\t\t\tHeight(8).\n\t\t\t\tTitleFunc(func() string {\n\t\t\t\t\tswitch country {\n\t\t\t\t\tcase \"United States\":\n\t\t\t\t\t\treturn \"State\"\n\t\t\t\t\tcase \"Canada\":\n\t\t\t\t\t\treturn \"Province\"\n\t\t\t\t\tdefault:\n\t\t\t\t\t\treturn \"Territory\"\n\t\t\t\t\t}\n\t\t\t\t}, &country).\n\t\t\t\tOptionsFunc(func() []huh.Option[string] {\n\t\t\t\t\ts := states[country]\n\t\t\t\t\t// simulate API call\n\t\t\t\t\ttime.Sleep(1000 * time.Millisecond)\n\t\t\t\t\treturn huh.NewOptions(s...)\n\t\t\t\t}, &country /* only this function when `country` changes */),\n\t\t),\n\t)\n\n\terr := form.Run()\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\tfmt.Printf(\"%s, %s\\n\", state, country)\n}\n\nvar states = map[string][]string{\n\t\"Canada\": {\n\t\t\"Alberta\",\n\t\t\"British Columbia\",\n\t\t\"Manitoba\",\n\t\t\"New Brunswick\",\n\t\t\"Newfoundland and Labrador\",\n\t\t\"North West Territories\",\n\t\t\"Nova Scotia\",\n\t\t\"Nunavut\",\n\t\t\"Ontario\",\n\t\t\"Prince Edward Island\",\n\t\t\"Quebec\",\n\t\t\"Saskatchewan\",\n\t\t\"Yukon\",\n\t},\n\t\"Mexico\": {\n\t\t\"Aguascalientes\",\n\t\t\"Baja California\",\n\t\t\"Baja California Sur\",\n\t\t\"Campeche\",\n\t\t\"Chiapas\",\n\t\t\"Chihuahua\",\n\t\t\"Coahuila\",\n\t\t\"Colima\",\n\t\t\"Durango\",\n\t\t\"Guanajuato\",\n\t\t\"Guerrero\",\n\t\t\"Hidalgo\",\n\t\t\"Jalisco\",\n\t\t\"México\",\n\t\t\"Mexico City\",\n\t\t\"Michoacán\",\n\t\t\"Morelos\",\n\t\t\"Nayarit\",\n\t\t\"Nuevo León\",\n\t\t\"Oaxaca\",\n\t\t\"Puebla\",\n\t\t\"Querétaro\",\n\t\t\"Quintana Roo\",\n\t\t\"San Luis Potosí\",\n\t\t\"Sinaloa\",\n\t\t\"Sonora\",\n\t\t\"Tabasco\",\n\t\t\"Tamaulipas\",\n\t\t\"Tlaxcala\",\n\t\t\"Veracruz\",\n\t\t\"Ignacio de la Llave\",\n\t\t\"Yucatán\",\n\t\t\"Zacatecas\",\n\t},\n\t\"United States\": {\n\t\t\"Alabama\",\n\t\t\"Alaska\",\n\t\t\"Arizona\",\n\t\t\"Arkansas\",\n\t\t\"California\",\n\t\t\"Colorado\",\n\t\t\"Connecticut\",\n\t\t\"Delaware\",\n\t\t\"Florida\",\n\t\t\"Georgia\",\n\t\t\"Hawaii\",\n\t\t\"Idaho\",\n\t\t\"Illinois\",\n\t\t\"Indiana\",\n\t\t\"Iowa\",\n\t\t\"Kansas\",\n\t\t\"Kentucky\",\n\t\t\"Louisiana\",\n\t\t\"Maine\",\n\t\t\"Maryland\",\n\t\t\"Massachusetts\",\n\t\t\"Michigan\",\n\t\t\"Minnesota\",\n\t\t\"Mississippi\",\n\t\t\"Missouri\",\n\t\t\"Montana\",\n\t\t\"Nebraska\",\n\t\t\"Nevada\",\n\t\t\"New Hampshire\",\n\t\t\"New Jersey\",\n\t\t\"New Mexico\",\n\t\t\"New York\",\n\t\t\"North Carolina\",\n\t\t\"North Dakota\",\n\t\t\"Ohio\",\n\t\t\"Oklahoma\",\n\t\t\"Oregon\",\n\t\t\"Pennsylvania\",\n\t\t\"Rhode Island\",\n\t\t\"South Carolina\",\n\t\t\"South Dakota\",\n\t\t\"Tennessee\",\n\t\t\"Texas\",\n\t\t\"Utah\",\n\t\t\"Vermont\",\n\t\t\"Virginia\",\n\t\t\"Washington\",\n\t\t\"West Virginia\",\n\t\t\"Wisconsin\",\n\t\t\"Wyoming\",\n\t},\n}\n"
  },
  {
    "path": "examples/dynamic/dynamic-increment/main.go",
    "content": "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++\n\t\t\ttime.Sleep(1 * time.Second)\n\t\t}\n\t}()\n\n\tdescriptionFunc := func() string {\n\t\treturn fmt.Sprintf(\"The count is: %d\", count)\n\t}\n\n\thuh.NewForm(huh.NewGroup(\n\t\thuh.NewInput().\n\t\t\tTitle(\"Fill in the input\").\n\t\t\tDescriptionFunc(descriptionFunc, &count),\n\t\thuh.NewInput().\n\t\t\tTitle(\"Fill in the input\").\n\t\t\tDescriptionFunc(descriptionFunc, &count),\n\t)).Run()\n}\n"
  },
  {
    "path": "examples/dynamic/dynamic-markdown/main.go",
    "content": "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.NewForm(\n\t\thuh.NewGroup(\n\t\t\thuh.NewText().Title(\"Markdown\").Value(&md),\n\t\t\thuh.NewNote().Height(20).Title(\"Preview\").\n\t\t\t\tDescriptionFunc(func() string {\n\t\t\t\t\tfmd, err := glamour.Render(md, \"dark\")\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn md\n\t\t\t\t\t}\n\t\t\t\t\treturn fmd\n\t\t\t\t}, &md),\n\t\t),\n\t).Run()\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n"
  },
  {
    "path": "examples/dynamic/dynamic-name/main.go",
    "content": "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.NewGroup(\n\t\t\thuh.NewInput().\n\t\t\t\tTitle(\"What's your name?\").\n\t\t\t\tPlaceholder(\"Frank\").\n\t\t\t\tValue(&name),\n\t\t\thuh.NewNote().\n\t\t\t\tTitleFunc(func() string {\n\t\t\t\t\tif name == \"\" {\n\t\t\t\t\t\treturn \"Hello!\"\n\t\t\t\t\t}\n\t\t\t\t\treturn fmt.Sprintf(\"Hello, %s!\", name)\n\t\t\t\t}, &name).\n\t\t\t\tDescriptionFunc(func() string {\n\t\t\t\t\tif name == \"\" {\n\t\t\t\t\t\treturn \"How are you?\"\n\t\t\t\t\t}\n\t\t\t\t\treturn fmt.Sprintf(\"Your name is %d characters long\", len(name))\n\t\t\t\t}, &name),\n\t\t\thuh.NewText().\n\t\t\t\tTitle(\"Biography.\").\n\t\t\t\tPlaceholderFunc(func() string {\n\t\t\t\t\tplaceholder := \"Tell me about yourself\"\n\t\t\t\t\tif name != \"\" {\n\t\t\t\t\t\tplaceholder += \", \" + name\n\t\t\t\t\t}\n\t\t\t\t\tplaceholder += \".\"\n\t\t\t\t\treturn placeholder\n\t\t\t\t}, &name),\n\t\t\thuh.NewConfirm().\n\t\t\t\tTitleFunc(func() string {\n\t\t\t\t\tif name == \"\" {\n\t\t\t\t\t\treturn \"Continue?\"\n\t\t\t\t\t}\n\t\t\t\t\treturn fmt.Sprintf(\"Continue, %s?\", name)\n\t\t\t\t}, &name).\n\t\t\t\tDescriptionFunc(func() string {\n\t\t\t\t\tif name == \"\" {\n\t\t\t\t\t\treturn \"Are you sure?\"\n\t\t\t\t\t}\n\t\t\t\t\treturn fmt.Sprintf(\"Last chance, %s.\", name)\n\t\t\t\t}, &name),\n\t\t),\n\t).Run()\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\tfmt.Println(\"Until next time, \" + name + \"!\")\n}\n"
  },
  {
    "path": "examples/dynamic/dynamic-suggestions/main.go",
    "content": "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\n\tvar repo string\n\n\terr := huh.NewForm(\n\t\thuh.NewGroup(\n\t\t\thuh.NewInput().\n\t\t\t\tValue(&org).\n\t\t\t\tTitle(\"Organization\").\n\t\t\t\tPlaceholder(\"charmbracelet\"),\n\t\t\thuh.NewInput().\n\t\t\t\tValue(&repo).\n\t\t\t\tTitle(\"Repository\").\n\t\t\t\tPlaceholderFunc(func() string {\n\t\t\t\t\tswitch org {\n\t\t\t\t\tcase \"hashicorp\":\n\t\t\t\t\t\treturn \"terraform\"\n\t\t\t\t\tcase \"golang\":\n\t\t\t\t\t\treturn \"go\"\n\t\t\t\t\tdefault: // charmbracelet\n\t\t\t\t\t\treturn \"bubbletea\"\n\t\t\t\t\t}\n\t\t\t\t}, &org).\n\t\t\t\tSuggestionsFunc(func() []string {\n\t\t\t\t\tswitch org {\n\t\t\t\t\tcase \"charmbracelet\":\n\t\t\t\t\t\treturn []string{\"bubbletea\", \"huh\", \"mods\", \"melt\", \"freeze\", \"gum\", \"vhs\", \"pop\", \"lipgloss\", \"harmonica\"}\n\t\t\t\t\tcase \"hashicorp\":\n\t\t\t\t\t\treturn []string{\"terraform\", \"vault\", \"waypoint\"}\n\t\t\t\t\tcase \"golang\":\n\t\t\t\t\t\treturn []string{\"go\", \"net\", \"sys\", \"text\", \"tools\"}\n\t\t\t\t\tdefault:\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t}\n\t\t\t\t}, &org),\n\t\t),\n\t).Run()\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\tspinner.New().Title(fmt.Sprintf(\"Cloning %s/%s...\", org, repo)).Run()\n}\n"
  },
  {
    "path": "examples/filepicker/artichoke.hs",
    "content": ""
  },
  {
    "path": "examples/filepicker/demo.tape",
    "content": "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 \"./file\" Sleep .5s\nEnter\n\nSleep 1s\nType \"Frank\" Sleep 500ms\nEnter\n\nSleep 1s\nType \"_frank\" Sleep 500ms\nEnter\n\nSleep 1s\nEnter\nSleep 1s\nType@200ms \"jjjj\"\nSleep 1s\nEnter\n\nSleep 1.5s\nType \"hunter2\"\nSleep 4s\n"
  },
  {
    "path": "examples/filepicker/main.go",
    "content": "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.NewInput().\n\t\t\t\tTitle(\"Name\").\n\t\t\t\tDescription(\"What's your name?\"),\n\n\t\t\thuh.NewInput().\n\t\t\t\tTitle(\"Username\").\n\t\t\t\tDescription(\"Select your username.\"),\n\n\t\t\thuh.NewFilePicker().\n\t\t\t\tTitle(\"Profile\").\n\t\t\t\tDescription(\"Select your profile picture.\").\n\t\t\t\tAllowedTypes([]string{\".png\", \".jpeg\", \".webp\", \".gif\"}).\n\t\t\t\tValue(&file),\n\n\t\t\thuh.NewInput().\n\t\t\t\tTitle(\"Password\").\n\t\t\t\tEchoMode(huh.EchoModePassword).\n\t\t\t\tDescription(\"Set your Password.\"),\n\t\t),\n\t).WithShowHelp(true).Run()\n}\n"
  },
  {
    "path": "examples/filepicker-picking/main.go",
    "content": "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\thuh.NewFilePicker().\n\t\t\t\tPicking(true).\n\t\t\t\tTitle(\"Code\").\n\t\t\t\tDescription(\"Select a .go file\").\n\t\t\t\tAllowedTypes([]string{\".go\"}).\n\t\t\t\tValue(&file),\n\t\t),\n\t).WithShowHelp(true).Run()\n\tfmt.Println(file)\n}\n"
  },
  {
    "path": "examples/gh/create.go",
    "content": "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)\n\ntype Action int\n\nconst (\n\tCancel Action = iota\n\tPush\n\tFork\n\tSkip\n)\n\nvar highlight = lipgloss.NewStyle().Foreground(lipgloss.Color(\"#00D7D7\"))\n\nfunc customTheme(isDark bool) *huh.Styles {\n\ttheme := huh.ThemeBase16(isDark)\n\ttheme.FieldSeparator = lipgloss.NewStyle().SetString(\"\\n\")\n\ttheme.Help.FullKey.MarginTop(1)\n\treturn theme\n}\n\nfunc main() {\n\tvar action Action\n\n\trepo := \"charmbracelet/huh\"\n\n\ttheme := spinner.ThemeFunc(func(isDark bool) *spinner.Styles {\n\t\td := spinner.ThemeDefault(isDark)\n\t\td.Spinner = lipgloss.NewStyle().Foreground(lipgloss.Color(\"4\"))\n\t\treturn d\n\t})\n\n\tf := huh.NewForm(\n\t\thuh.NewGroup(\n\t\t\thuh.NewSelect[Action]().\n\t\t\t\tValue(&action).\n\t\t\t\tOptions(\n\t\t\t\t\thuh.NewOption(repo, Push),\n\t\t\t\t\thuh.NewOption(\"Create a fork of \"+repo, Fork),\n\t\t\t\t\thuh.NewOption(\"Skip pushing the branch\", Skip),\n\t\t\t\t\thuh.NewOption(\"Cancel\", Cancel),\n\t\t\t\t).\n\t\t\t\tTitle(\"Where should we push the 'feature' branch?\"),\n\t\t),\n\t).WithTheme(huh.ThemeFunc(customTheme))\n\n\terr := f.Run()\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\tswitch action {\n\tcase Push:\n\t\t_ = spinner.New().Title(\"Pushing to charmbracelet/huh\").WithTheme(theme).Run()\n\t\tfmt.Println(\"Pushed to charmbracelet/huh\")\n\tcase Fork:\n\t\tfmt.Println(\"Creating a fork of charmbracelet/huh...\")\n\tcase Skip:\n\t\tfmt.Println(\"Skipping pushing the branch...\")\n\tcase Cancel:\n\t\tfmt.Println(\"Cancelling...\")\n\t\tos.Exit(1)\n\t}\n\n\tfmt.Printf(\"Creating pull request for %s into %s in %s\\n\", highlight.Render(\"test\"), highlight.Render(\"main\"), repo)\n\n\tvar nextAction string\n\n\tf = huh.NewForm(\n\t\thuh.NewGroup(\n\t\t\thuh.NewInput().\n\t\t\t\tTitle(\"Title \").\n\t\t\t\tPrompt(\"\").\n\t\t\t\tInline(true),\n\t\t\thuh.NewText().\n\t\t\t\tTitle(\"Body\"),\n\t\t),\n\t\thuh.NewGroup(\n\t\t\thuh.NewSelect[string]().\n\t\t\t\tOptions(huh.NewOptions(\"Submit\", \"Submit as draft\", \"Continue in browser\", \"Add metadata\", \"Cancel\")...).\n\t\t\t\tTitle(\"What's next?\").Value(&nextAction),\n\t\t),\n\t).WithTheme(huh.ThemeFunc(customTheme))\n\n\terr = f.Run()\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\tif nextAction == \"Submit\" {\n\t\t_ = spinner.New().Title(\"Submitting...\").WithTheme(theme).Run()\n\t\tfmt.Println(\"Pull request submitted!\")\n\t}\n}\n"
  },
  {
    "path": "examples/git/main.go",
    "content": "package main\n\nimport (\n\t\"charm.land/huh/v2\"\n)\n\n// types is the possible commit types specified by the conventional commit spec.\nvar types = []string{\"fix\", \"feat\", \"docs\", \"style\", \"refactor\", \"test\", \"chore\", \"revert\"}\n\n// This form is used to write a conventional commit message. It prompts the user\n// to choose the type of commit as specified in the conventional commit spec.\n// And then prompts for the summary and detailed description of the message and\n// uses the values provided as the summary and details of the message.\nfunc main() {\n\tvar commit, scope string\n\tvar summary, description string\n\tvar confirm bool\n\n\thuh.NewForm(\n\t\thuh.NewGroup(\n\t\t\thuh.NewInput().Title(\"Type\").Value(&commit).Placeholder(\"feat\").Suggestions(types),\n\t\t\thuh.NewInput().Title(\"Scope\").Value(&scope).Placeholder(\"scope\"),\n\t\t),\n\t\thuh.NewGroup(\n\t\t\thuh.NewInput().Title(\"Summary\").Value(&summary).Placeholder(\"Summary of changes\"),\n\t\t\thuh.NewText().Title(\"Description\").Value(&description).Placeholder(\"Detailed description of changes\"),\n\t\t),\n\t\thuh.NewGroup(huh.NewConfirm().Title(\"Commit changes?\").Value(&confirm)),\n\t).Run()\n}\n"
  },
  {
    "path": "examples/go.mod",
    "content": "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/bubbletea/v2 v2.0.2\n\tcharm.land/glamour/v2 v2.0.0\n\tcharm.land/huh/v2 v2.0.0-00010101000000-000000000000\n\tcharm.land/lipgloss/v2 v2.0.2\n\tcharm.land/log/v2 v2.0.0\n\tcharm.land/wish/v2 v2.0.0\n\tgithub.com/charmbracelet/ssh v0.0.0-20250826160808-ebfa259c7309\n\tgithub.com/charmbracelet/x/exp/strings v0.1.0\n)\n\nrequire (\n\tgithub.com/alecthomas/chroma/v2 v2.14.0 // indirect\n\tgithub.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect\n\tgithub.com/atotto/clipboard v0.1.4 // indirect\n\tgithub.com/aymerick/douceur v0.2.0 // indirect\n\tgithub.com/catppuccin/go v0.3.0 // indirect\n\tgithub.com/charmbracelet/colorprofile v0.4.2 // indirect\n\tgithub.com/charmbracelet/harmonica v0.2.0 // indirect\n\tgithub.com/charmbracelet/keygen v0.5.4 // indirect\n\tgithub.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect\n\tgithub.com/charmbracelet/x/ansi v0.11.6 // indirect\n\tgithub.com/charmbracelet/x/conpty v0.2.0 // indirect\n\tgithub.com/charmbracelet/x/exp/ordered v0.1.0 // indirect\n\tgithub.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect\n\tgithub.com/charmbracelet/x/term v0.2.2 // indirect\n\tgithub.com/charmbracelet/x/termios v0.1.1 // indirect\n\tgithub.com/charmbracelet/x/windows v0.2.2 // indirect\n\tgithub.com/clipperhouse/displaywidth v0.11.0 // indirect\n\tgithub.com/clipperhouse/uax29/v2 v2.7.0 // indirect\n\tgithub.com/creack/pty v1.1.24 // indirect\n\tgithub.com/dlclark/regexp2 v1.11.0 // indirect\n\tgithub.com/dustin/go-humanize v1.0.1 // indirect\n\tgithub.com/go-logfmt/logfmt v0.6.1 // indirect\n\tgithub.com/gorilla/css v1.0.1 // indirect\n\tgithub.com/lucasb-eyer/go-colorful v1.3.0 // indirect\n\tgithub.com/mattn/go-runewidth v0.0.20 // indirect\n\tgithub.com/microcosm-cc/bluemonday v1.0.27 // indirect\n\tgithub.com/mitchellh/hashstructure/v2 v2.0.2 // indirect\n\tgithub.com/muesli/cancelreader v0.2.2 // indirect\n\tgithub.com/rivo/uniseg v0.4.7 // indirect\n\tgithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect\n\tgithub.com/yuin/goldmark v1.7.8 // indirect\n\tgithub.com/yuin/goldmark-emoji v1.0.5 // indirect\n\tgolang.org/x/crypto v0.48.0 // indirect\n\tgolang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 // indirect\n\tgolang.org/x/net v0.49.0 // indirect\n\tgolang.org/x/sync v0.20.0 // indirect\n\tgolang.org/x/sys v0.42.0 // indirect\n\tgolang.org/x/text v0.34.0 // indirect\n)\n"
  },
  {
    "path": "examples/go.sum",
    "content": "charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s=\ncharm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI=\ncharm.land/bubbletea/v2 v2.0.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0=\ncharm.land/bubbletea/v2 v2.0.2/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ=\ncharm.land/glamour/v2 v2.0.0 h1:IDBoqLEy7Hdpb9VOXN+khLP/XSxtJy1VsHuW/yF87+U=\ncharm.land/glamour/v2 v2.0.0/go.mod h1:kjq9WB0s8vuUYZNYey2jp4Lgd9f4cKdzAw88FZtpj/w=\ncharm.land/lipgloss/v2 v2.0.2 h1:xFolbF8JdpNkM2cEPTfXEcW1p6NRzOWTSamRfYEw8cs=\ncharm.land/lipgloss/v2 v2.0.2/go.mod h1:KjPle2Qd3YmvP1KL5OMHiHysGcNwq6u83MUjYkFvEkM=\ncharm.land/log/v2 v2.0.0 h1:SY3Cey7ipx86/MBXQHwsguOT6X1exT94mmJRdzTNs+s=\ncharm.land/log/v2 v2.0.0/go.mod h1:c3cZSRqm20qUVVAR1WmS/7ab8bgha3C6G7DjPcaVZz0=\ncharm.land/wish/v2 v2.0.0 h1:0vryoDz6G1SdJNIWSkExy88dLAs7H/w0x9y/cay1vno=\ncharm.land/wish/v2 v2.0.0/go.mod h1:B42DmuVdvQxz215H9aCsbrXVSuAInAqkHAnmwg0nKs8=\ngithub.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=\ngithub.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=\ngithub.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=\ngithub.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=\ngithub.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=\ngithub.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=\ngithub.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=\ngithub.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=\ngithub.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=\ngithub.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=\ngithub.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=\ngithub.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=\ngithub.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=\ngithub.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=\ngithub.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=\ngithub.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=\ngithub.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=\ngithub.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=\ngithub.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY=\ngithub.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8=\ngithub.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=\ngithub.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=\ngithub.com/charmbracelet/keygen v0.5.4 h1:XQYgf6UEaTGgQSSmiPpIQ78WfseNQp4Pz8N/c1OsrdA=\ngithub.com/charmbracelet/keygen v0.5.4/go.mod h1:t4oBRr41bvK7FaJsAaAQhhkUuHslzFXVjOBwA55CZNM=\ngithub.com/charmbracelet/ssh v0.0.0-20250826160808-ebfa259c7309 h1:dCVbCRRtg9+tsfiTXTp0WupDlHruAXyp+YoxGVofHHc=\ngithub.com/charmbracelet/ssh v0.0.0-20250826160808-ebfa259c7309/go.mod h1:R9cISUs5kAH4Cq/rguNbSwcR+slE5Dfm8FEs//uoIGE=\ngithub.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 h1:eyFRbAmexyt43hVfeyBofiGSEmJ7krjLOYt/9CF5NKA=\ngithub.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8/go.mod h1:SQpCTRNBtzJkwku5ye4S3HEuthAlGy2n9VXZnWkEW98=\ngithub.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=\ngithub.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=\ngithub.com/charmbracelet/x/conpty v0.2.0 h1:eKtA2hm34qNfgJCDp/M6Dc0gLy7e07YEK4qAdNGOvVY=\ngithub.com/charmbracelet/x/conpty v0.2.0/go.mod h1:fexgUnVrZgw8scD49f6VSi0Ggj9GWYIrpedRthAwW/8=\ngithub.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=\ngithub.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=\ngithub.com/charmbracelet/x/exp/ordered v0.1.0 h1:55/qLwjIh0gL0Vni+QAWk7T/qRVP6sBf+2agPBgnOFE=\ngithub.com/charmbracelet/x/exp/ordered v0.1.0/go.mod h1:5UHwmG+is5THxMyCJHNPCn2/ecI07aKNrW+LcResjJ8=\ngithub.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI=\ngithub.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU=\ngithub.com/charmbracelet/x/exp/strings v0.1.0 h1:i69S2XI7uG1u4NLGeJPSYU++Nmjvpo9nwd6aoEm7gkA=\ngithub.com/charmbracelet/x/exp/strings v0.1.0/go.mod h1:/ehtMPNh9K4odGFkqYJKpIYyePhdp1hLBRvyY4bWkH8=\ngithub.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=\ngithub.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=\ngithub.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=\ngithub.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=\ngithub.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=\ngithub.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=\ngithub.com/charmbracelet/x/xpty v0.1.3 h1:eGSitii4suhzrISYH50ZfufV3v085BXQwIytcOdFSsw=\ngithub.com/charmbracelet/x/xpty v0.1.3/go.mod h1:poPYpWuLDBFCKmKLDnhBp51ATa0ooD8FhypRwEFtH3Y=\ngithub.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=\ngithub.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=\ngithub.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=\ngithub.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=\ngithub.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=\ngithub.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=\ngithub.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=\ngithub.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=\ngithub.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=\ngithub.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE=\ngithub.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=\ngithub.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=\ngithub.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=\ngithub.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=\ngithub.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=\ngithub.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=\ngithub.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=\ngithub.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=\ngithub.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=\ngithub.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=\ngithub.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=\ngithub.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=\ngithub.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=\ngithub.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=\ngithub.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=\ngithub.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=\ngithub.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=\ngithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=\ngithub.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=\ngithub.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=\ngithub.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=\ngithub.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=\ngithub.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=\ngolang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=\ngolang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=\ngolang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 h1:zfMcR1Cs4KNuomFFgGefv5N0czO2XZpUbxGUy8i8ug0=\ngolang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=\ngolang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=\ngolang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=\ngolang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=\ngolang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=\ngolang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=\ngolang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=\ngolang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=\ngolang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=\ngolang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "examples/gum/main.go",
    "content": "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 <input | text>\")\n\t\tos.Exit(1)\n\t}\n\tswitch os.Args[1] {\n\tcase \"input\":\n\t\thuh.NewInput().Run()\n\tcase \"text\":\n\t\thuh.NewText().Run()\n\tcase \"confirm\":\n\t\thuh.NewConfirm().Run()\n\tcase \"select\":\n\t\thuh.NewSelect[string]().Options(huh.NewOptions(os.Args[2:]...)...).Run()\n\tcase \"multiselect\":\n\t\thuh.NewMultiSelect[string]().Options(huh.NewOptions(os.Args[2:]...)...).Run()\n\t}\n}\n"
  },
  {
    "path": "examples/help/main.go",
    "content": "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(\"Dynamic Help\"),\n\t\t\thuh.NewInput().Title(\"Dynamic Help\"),\n\t\t\thuh.NewInput().Title(\"Dynamic Help\"),\n\t\t),\n\t)\n\tf.Run()\n}\n"
  },
  {
    "path": "examples/hide/hide.tape",
    "content": "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  Enter\n  Ctrl+L\n  Sleep 500ms\n  Type \"clear && ./hide\"\n  Sleep 500ms\n  Enter\n  Sleep 500ms\nShow\n\nType@500ms \"llllllll\"\n\nHide\nType \"rm hide\" Enter\n\n"
  },
  {
    "path": "examples/hide/main.go",
    "content": "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.NewForm(\n\t\thuh.NewGroup(huh.NewNote().Title(\"Just for fun!\")).WithHideFunc(func() bool { return true }),\n\t\thuh.NewGroup(huh.NewNote().Title(\"Just for fun!\")).WithHide(true),\n\n\t\thuh.NewGroup(huh.NewConfirm().\n\t\t\tTitle(\"Do you have any allergies?\").\n\t\t\tDescription(\"If so, please list them.\").\n\t\t\tValue(&isAllergic)),\n\t\thuh.NewGroup(\n\t\t\thuh.NewText().\n\t\t\t\tTitle(\"Allergies\").\n\t\t\t\tDescription(\"Please list all your allergies...\").\n\t\t\t\tValue(&allergies),\n\t\t).WithHideFunc(func() bool {\n\t\t\treturn !isAllergic\n\t\t}),\n\t\thuh.NewGroup(huh.NewNote().Title(\"Invisible\")).WithHide(true),\n\t).Run()\n\n\tif isAllergic {\n\t\tfmt.Println(allergies)\n\t}\n}\n"
  },
  {
    "path": "examples/layout/columns/main.go",
    "content": "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(\"First\"),\n\t\t\thuh.NewInput().Title(\"Second\"),\n\t\t\thuh.NewInput().Title(\"Third\"),\n\t\t),\n\t\thuh.NewGroup(\n\t\t\thuh.NewInput().Title(\"Fourth\"),\n\t\t\thuh.NewInput().Title(\"Fifth\"),\n\t\t\thuh.NewInput().Title(\"Sixth\"),\n\t\t),\n\t\thuh.NewGroup(\n\t\t\thuh.NewInput().Title(\"Seventh\"),\n\t\t\thuh.NewInput().Title(\"Eighth\"),\n\t\t\thuh.NewInput().Title(\"Nineth\"),\n\t\t\thuh.NewInput().Title(\"Tenth\"),\n\t\t),\n\t).WithLayout(huh.LayoutColumns(2))\n\tform.Run()\n}\n"
  },
  {
    "path": "examples/layout/default/main.go",
    "content": "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(\"First\"),\n\t\t\thuh.NewInput().Title(\"Second\"),\n\t\t\thuh.NewInput().Title(\"Third\"),\n\t\t),\n\t\thuh.NewGroup(\n\t\t\thuh.NewInput().Title(\"Fourth\"),\n\t\t\thuh.NewInput().Title(\"Fifth\"),\n\t\t\thuh.NewInput().Title(\"Sixth\"),\n\t\t),\n\t\thuh.NewGroup(\n\t\t\thuh.NewInput().Title(\"Seventh\"),\n\t\t\thuh.NewInput().Title(\"Eighth\"),\n\t\t\thuh.NewInput().Title(\"Nineth\"),\n\t\t\thuh.NewInput().Title(\"Tenth\"),\n\t\t),\n\t)\n\tform.Run()\n}\n"
  },
  {
    "path": "examples/layout/grid/main.go",
    "content": "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(\"First\"),\n\t\t\thuh.NewInput().Title(\"Second\"),\n\t\t\thuh.NewInput().Title(\"Third\"),\n\t\t),\n\t\thuh.NewGroup(\n\t\t\thuh.NewInput().Title(\"Fourth\"),\n\t\t\thuh.NewInput().Title(\"Fifth\"),\n\t\t\thuh.NewInput().Title(\"Sixth\"),\n\t\t),\n\t\thuh.NewGroup(\n\t\t\thuh.NewInput().Title(\"Seventh\"),\n\t\t\thuh.NewInput().Title(\"Eighth\"),\n\t\t\thuh.NewInput().Title(\"Nineth\"),\n\t\t\thuh.NewInput().Title(\"Tenth\"),\n\t\t),\n\t\thuh.NewGroup(\n\t\t\thuh.NewInput().Title(\"Eleventh\"),\n\t\t\thuh.NewInput().Title(\"Twelveth\"),\n\t\t\thuh.NewInput().Title(\"Thirteenth\"),\n\t\t),\n\t).WithLayout(huh.LayoutGrid(2, 2))\n\tform.Run()\n}\n"
  },
  {
    "path": "examples/layout/stack/main.go",
    "content": "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(\"First\"),\n\t\t\thuh.NewInput().Title(\"Second\"),\n\t\t\thuh.NewInput().Title(\"Third\"),\n\t\t),\n\t\thuh.NewGroup(\n\t\t\thuh.NewInput().Title(\"Fourth\"),\n\t\t\thuh.NewInput().Title(\"Fifth\"),\n\t\t\thuh.NewInput().Title(\"Sixth\"),\n\t\t),\n\t\thuh.NewGroup(\n\t\t\thuh.NewInput().Title(\"Seventh\"),\n\t\t\thuh.NewInput().Title(\"Eighth\"),\n\t\t\thuh.NewInput().Title(\"Nineth\"),\n\t\t\thuh.NewInput().Title(\"Tenth\"),\n\t\t),\n\t).WithLayout(huh.LayoutStack)\n\tform.Run()\n}\n"
  },
  {
    "path": "examples/multiple-groups/main.go",
    "content": "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.NewSelect[string]().\n\t\t\t\tOptions(\n\t\t\t\t\thuh.NewOption(\"A\", \"a\"),\n\t\t\t\t\thuh.NewOption(\"B\", \"b\"),\n\t\t\t\t\thuh.NewOption(\"C\", \"c\"),\n\t\t\t\t\thuh.NewOption(\"D\", \"d\"),\n\t\t\t\t\thuh.NewOption(\"E\", \"e\"),\n\t\t\t\t\thuh.NewOption(\"F\", \"f\"),\n\t\t\t\t\thuh.NewOption(\"G\", \"g\"),\n\t\t\t\t\thuh.NewOption(\"H\", \"h\"),\n\t\t\t\t\thuh.NewOption(\"I\", \"i\"),\n\t\t\t\t\thuh.NewOption(\"J\", \"j\"),\n\t\t\t\t\thuh.NewOption(\"K\", \"k\").Selected(true),\n\t\t\t\t\thuh.NewOption(\"L\", \"l\"),\n\t\t\t\t\thuh.NewOption(\"M\", \"m\"),\n\t\t\t\t\thuh.NewOption(\"N\", \"n\"),\n\t\t\t\t\thuh.NewOption(\"O\", \"o\"),\n\t\t\t\t\thuh.NewOption(\"P\", \"p\"),\n\t\t\t\t),\n\t\t).WithHeight(8),\n\t\thuh.NewGroup(\n\t\t\thuh.NewMultiSelect[string]().\n\t\t\t\tOptions(\n\t\t\t\t\thuh.NewOption(\"A\", \"a\"),\n\t\t\t\t\thuh.NewOption(\"B\", \"b\"),\n\t\t\t\t\thuh.NewOption(\"C\", \"c\"),\n\t\t\t\t\thuh.NewOption(\"D\", \"d\"),\n\t\t\t\t\thuh.NewOption(\"E\", \"e\"),\n\t\t\t\t\thuh.NewOption(\"F\", \"f\"),\n\t\t\t\t\thuh.NewOption(\"G\", \"g\"),\n\t\t\t\t\thuh.NewOption(\"H\", \"h\"),\n\t\t\t\t\thuh.NewOption(\"I\", \"i\"),\n\t\t\t\t\thuh.NewOption(\"K\", \"k\").Selected(true),\n\t\t\t\t\thuh.NewOption(\"L\", \"l\"),\n\t\t\t\t\thuh.NewOption(\"M\", \"m\"),\n\t\t\t\t\thuh.NewOption(\"N\", \"n\"),\n\t\t\t\t\thuh.NewOption(\"O\", \"o\").Selected(true),\n\t\t\t\t\thuh.NewOption(\"P\", \"p\"),\n\t\t\t\t),\n\t\t).WithHeight(10),\n\t\thuh.NewGroup(\n\t\t\thuh.NewSelect[string]().\n\t\t\t\tOptions(\n\t\t\t\t\thuh.NewOption(\"A\", \"a\"),\n\t\t\t\t\thuh.NewOption(\"B\", \"b\"),\n\t\t\t\t\thuh.NewOption(\"C\", \"c\"),\n\t\t\t\t\thuh.NewOption(\"D\", \"d\"),\n\t\t\t\t\thuh.NewOption(\"E\", \"e\"),\n\t\t\t\t\thuh.NewOption(\"F\", \"f\"),\n\t\t\t\t\thuh.NewOption(\"G\", \"g\"),\n\t\t\t\t\thuh.NewOption(\"H\", \"h\"),\n\t\t\t\t\thuh.NewOption(\"I\", \"i\"),\n\t\t\t\t\thuh.NewOption(\"J\", \"j\"),\n\t\t\t\t\thuh.NewOption(\"K\", \"k\").Selected(true),\n\t\t\t\t\thuh.NewOption(\"L\", \"l\"),\n\t\t\t\t\thuh.NewOption(\"M\", \"m\"),\n\t\t\t\t\thuh.NewOption(\"N\", \"n\"),\n\t\t\t\t\thuh.NewOption(\"O\", \"o\"),\n\t\t\t\t\thuh.NewOption(\"P\", \"p\"),\n\t\t\t\t),\n\t\t).WithHeight(5),\n\t)\n\n\tif err := f.Run(); err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"Oof: %v\\n\", err)\n\t}\n}\n"
  },
  {
    "path": "examples/readme/confirm/confirm.tape",
    "content": "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\n  Enter\n  Ctrl+L\n  Sleep 500ms\n  Type \"clear && ./confirm\"\n  Sleep 500ms\n  Enter\n  Sleep 500ms\nShow\n\nSleep 1s\nLeft@500ms 6\nSleep 2s\n\nHide\nType \"rm confirm\"\nEnter\nSleep 500ms\n"
  },
  {
    "path": "examples/readme/confirm/main.go",
    "content": "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 you sure? \").\n\t\tDescription(\"Please confirm. \").\n\t\tAffirmative(\"Yes!\").\n\t\tNegative(\"No.\").\n\t\tValue(&happy)\n\n\thuh.NewForm(huh.NewGroup(confirm)).Run()\n}\n"
  },
  {
    "path": "examples/readme/input/input.tape",
    "content": "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  Enter\n  Ctrl+L\n  Sleep 500ms\n  Type \"./input\"\n  Sleep 500ms\n  Enter\n  Sleep 500ms\nShow\n\nSleep 1s\nType \"Spaghetti\"\nSleep 2s\n\nHide\nType \"rm input\"\nEnter\n"
  },
  {
    "path": "examples/readme/input/main.go",
    "content": "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 lunch string\n\n\tinput := huh.NewInput().\n\t\tTitle(\"What's for lunch?\").\n\t\tPrompt(\"? \").\n\t\tSuggestions([]string{\n\t\t\t\"Artichoke\",\n\t\t\t\"Baking Flour\",\n\t\t\t\"Bananas\",\n\t\t\t\"Barley\",\n\t\t\t\"Bean Sprouts\",\n\t\t\t\"Bitter Melon\",\n\t\t\t\"Black Cod\",\n\t\t\t\"Blood Orange\",\n\t\t\t\"Brown Sugar\",\n\t\t\t\"Cashew Apple\",\n\t\t\t\"Cashews\",\n\t\t\t\"Cat Food\",\n\t\t\t\"Coconut Milk\",\n\t\t\t\"Cucumber\",\n\t\t\t\"Curry Paste\",\n\t\t\t\"Currywurst\",\n\t\t\t\"Dill\",\n\t\t\t\"Dragonfruit\",\n\t\t\t\"Dried Shrimp\",\n\t\t\t\"Eggs\",\n\t\t\t\"Fish Cake\",\n\t\t\t\"Furikake\",\n\t\t\t\"Garlic\",\n\t\t}).\n\t\tValidate(isFood).\n\t\tValue(&lunch)\n\n\thuh.NewForm(huh.NewGroup(input)).Run()\n\n\tfmt.Printf(\"Yummy, %s!\\n\", lunch)\n}\n"
  },
  {
    "path": "examples/readme/input/suggestions.tape",
    "content": "Output suggestions.gif\n\nSet Width 1000\nSet Padding 30\nSet Height 275\nSet FontSize 38\n\nHide\n  Type \"go build .\"\n  Sleep 500ms\n  Enter\n  Ctrl+L\n  Sleep 500ms\n  Type \"./input\"\n  Sleep 500ms\n  Enter\n  Sleep 500ms\nShow\n\nSleep 1s\nType@300ms \"Curryw\"\nSleep 1.5s\nCtrl+E\nSleep 3s\n\nHide\nType \"rm input\"\nEnter\nSleep 0.5s\n\n"
  },
  {
    "path": "examples/readme/main/main.go",
    "content": "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(s string) error { return nil }\n\n// TODO: ensure input is food.\nfunc isFood(s string) error { return nil }\n\n// TODO: ensure input is a valid name.\nfunc validateName(s string) error { return nil }\n\nfunc main() {\n\tvar (\n\t\tlunch    string\n\t\tstory    string\n\t\tcountry  string\n\t\ttoppings []string\n\t\tdiscount bool\n\t)\n\n\t// `Input`s are single line text fields.\n\thuh.NewInput().\n\t\tTitle(\"What's for lunch?\").\n\t\tPrompt(\"?\").\n\t\tValidate(isFood).\n\t\tValue(&lunch)\n\n\t// `Text`s are multi-line text fields.\n\thuh.NewText().\n\t\tTitle(\"Tell me a story.\").\n\t\tValidate(checkForPlagiarism).\n\t\tValue(&story)\n\n\t// `Select`s are multiple choice questions.\n\thuh.NewSelect[string]().\n\t\tTitle(\"Pick a country.\").\n\t\tOptions(\n\t\t\thuh.NewOption(\"United States\", \"US\"),\n\t\t\thuh.NewOption(\"Germany\", \"DE\"),\n\t\t\thuh.NewOption(\"Brazil\", \"BR\"),\n\t\t\thuh.NewOption(\"Canada\", \"CA\"),\n\t\t).\n\t\tValue(&country)\n\n\t// `MultiSelect`s allow multiple selections from a list of options.\n\thuh.NewMultiSelect[string]().\n\t\tOptions(\n\t\t\thuh.NewOption(\"Cheese\", \"cheese\").Selected(true),\n\t\t\thuh.NewOption(\"Lettuce\", \"lettuce\").Selected(true),\n\t\t\thuh.NewOption(\"Corn\", \"corn\"),\n\t\t\thuh.NewOption(\"Salsa\", \"salsa\"),\n\t\t\thuh.NewOption(\"Sour Cream\", \"sour cream\"),\n\t\t\thuh.NewOption(\"Tomatoes\", \"tomatoes\"),\n\t\t).\n\t\tTitle(\"Toppings\").\n\t\tLimit(4).\n\t\tValue(&toppings)\n\n\t// `Confirm`s are a confirmation prompt.\n\thuh.NewConfirm().\n\t\tTitle(\"Want a discount?\").\n\t\tAffirmative(\"Yes!\").\n\t\tNegative(\"No.\").\n\t\tValue(&discount)\n\n\t// Form\n\tvar (\n\t\tburger       string\n\t\tname         string\n\t\tinstructions string\n\t)\n\n\tform := huh.NewForm(\n\t\t// Prompt the user to choose a burger.\n\t\thuh.NewGroup(\n\t\t\thuh.NewSelect[string]().\n\t\t\t\tOptions(\n\t\t\t\t\thuh.NewOption(\"Charmburger Classic\", \"classic\"),\n\t\t\t\t\thuh.NewOption(\"Chickwich\", \"chickwich\"),\n\t\t\t\t\thuh.NewOption(\"Fishburger\", \"Fishburger\"),\n\t\t\t\t\thuh.NewOption(\"Charmpossible™ Burger\", \"charmpossible\"),\n\t\t\t\t).\n\t\t\t\tTitle(\"Choose your burger\").\n\t\t\t\tValue(&burger),\n\t\t),\n\n\t\t// Prompt for toppings and special instructions.\n\t\t// The customer can ask for up to 4 toppings.\n\t\thuh.NewGroup(\n\t\t\thuh.NewMultiSelect[string]().\n\t\t\t\tOptions(\n\t\t\t\t\thuh.NewOption(\"Lettuce\", \"Lettuce\").Selected(true),\n\t\t\t\t\thuh.NewOption(\"Tomatoes\", \"Tomatoes\").Selected(true),\n\t\t\t\t\thuh.NewOption(\"Charm Sauce\", \"Charm Sauce\"),\n\t\t\t\t\thuh.NewOption(\"Jalapeños\", \"Jalapeños\"),\n\t\t\t\t\thuh.NewOption(\"Cheese\", \"Cheese\"),\n\t\t\t\t\thuh.NewOption(\"Vegan Cheese\", \"Vegan Cheese\"),\n\t\t\t\t\thuh.NewOption(\"Nutella\", \"Nutella\"),\n\t\t\t\t).\n\t\t\t\tTitle(\"Toppings\").\n\t\t\t\tLimit(4).\n\t\t\t\tValue(&toppings),\n\t\t),\n\n\t\t// Gather final details for the order.\n\t\thuh.NewGroup(\n\t\t\thuh.NewInput().\n\t\t\t\tTitle(\"What's your name?\").\n\t\t\t\tValue(&name).\n\t\t\t\tValidate(validateName),\n\n\t\t\thuh.NewText().\n\t\t\t\tTitle(\"Special Instructions\").\n\t\t\t\tValue(&instructions).\n\t\t\t\tCharLimit(400),\n\n\t\t\thuh.NewConfirm().\n\t\t\t\tTitle(\"Would you like 15% off\").\n\t\t\t\tValue(&discount),\n\t\t),\n\t)\n\n\terr := form.Run()\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n"
  },
  {
    "path": "examples/readme/multiselect/main.go",
    "content": "package main\n\nimport \"charm.land/huh/v2\"\n\nfunc main() {\n\tvar toppings []string\n\ts := huh.NewMultiSelect[string]().\n\t\tOptions(\n\t\t\thuh.NewOption(\"Lettuce\", \"Lettuce\").Selected(true),\n\t\t\thuh.NewOption(\"Tomatoes\", \"Tomatoes\").Selected(true),\n\t\t\thuh.NewOption(\"Charm Sauce\", \"Charm Sauce\"),\n\t\t\thuh.NewOption(\"Jalapeños\", \"Jalapeños\"),\n\t\t\thuh.NewOption(\"Cheese\", \"Cheese\"),\n\t\t\thuh.NewOption(\"Vegan Cheese\", \"Vegan Cheese\"),\n\t\t\thuh.NewOption(\"Nutella\", \"Nutella\"),\n\t\t).\n\t\tTitle(\"Toppings\").\n\t\tLimit(4).\n\t\tValue(&toppings)\n\n\thuh.NewForm(huh.NewGroup(s)).Run()\n}\n"
  },
  {
    "path": "examples/readme/multiselect/multiselect.tape",
    "content": "Output multiselect.gif\n\nSet Width 1150\nSet Padding 40\nSet Height 480\nSet FontSize 28\n\nHide\n  Type \"go build .\"\n  Sleep 500ms\n  Enter\n  Ctrl+L\n  Sleep 500ms\n  Type \"clear && ./multiselect\"\n  Sleep 500ms\n  Enter\n  Sleep 500ms\nShow\n\nSleep 1s\nDown@500ms 3\nSleep 1s\nType \"x\"\nSleep 1s\nUp@500ms 2\nSleep 1s\n\nType \"x\"\nSleep 2s\n\nHide\nType \"rm multiselect\" Enter\nSleep 1s\n"
  },
  {
    "path": "examples/readme/note/main.go",
    "content": "package main\n\nimport \"charm.land/huh/v2\"\n\nfunc main() {\n\tnote := huh.NewNote().Description(\n\t\t\"# Heading\\n\" + \"This is _italic_, *bold*\" +\n\t\t\t\"\\n\\n# Heading\\n\" + \"`This is _italic_, *bold*`\",\n\t)\n\thuh.NewForm(\n\t\thuh.NewGroup(note),\n\t).Run()\n}\n"
  },
  {
    "path": "examples/readme/select/main.go",
    "content": "package main\n\nimport \"charm.land/huh/v2\"\n\nfunc main() {\n\tvar country string\n\ts := huh.NewSelect[string]().\n\t\tTitle(\"Pick a country.\").\n\t\tOptions(\n\t\t\thuh.NewOption(\"United States\", \"US\"),\n\t\t\thuh.NewOption(\"Germany\", \"DE\"),\n\t\t\thuh.NewOption(\"Brazil\", \"BR\"),\n\t\t\thuh.NewOption(\"Canada\", \"CA\"),\n\t\t).\n\t\tValue(&country)\n\n\thuh.NewForm(huh.NewGroup(s)).Run()\n}\n"
  },
  {
    "path": "examples/readme/select/scroll/scroll.go",
    "content": "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, \"Bulbasaur\"},\n\t{2, \"Ivysaur\"},\n\t{3, \"Venusaur\"},\n\t{4, \"Charmander\"},\n\t{5, \"Charmeleon\"},\n\t{6, \"Charizard\"},\n\t{7, \"Squirtle\"},\n\t{8, \"Wartortle\"},\n\t{9, \"Blastoise\"},\n\t{10, \"Caterpie\"},\n\t{11, \"Metapod\"},\n\t{12, \"Butterfree\"},\n\t{13, \"Weedle\"},\n\t{14, \"Kakuna\"},\n\t{15, \"Beedrill\"},\n\t{16, \"Pidgey\"},\n\t{17, \"Pidgeotto\"},\n\t{18, \"Pidgeot\"},\n\t{19, \"Rattata\"},\n\t{20, \"Raticate\"},\n\t{21, \"Spearow\"},\n\t{22, \"Fearow\"},\n\t{23, \"Ekans\"},\n\t{24, \"Arbok\"},\n\t{25, \"Pikachu\"},\n\t{26, \"Raichu\"},\n\t{27, \"Sandshrew\"},\n\t{28, \"Sandslash\"},\n}\n\nfunc (p Pokemon) String() string {\n\treturn p.name\n}\n\nfunc main() {\n\tvar pokemon Pokemon\n\n\ts := huh.NewSelect[Pokemon]().\n\t\tTitle(\"Choose your starter\").\n\t\tOptions(huh.NewOptions(pokemons...)...).\n\t\tValue(&pokemon).\n\t\tWithHeight(7)\n\n\thuh.NewForm(huh.NewGroup(s)).Run()\n}\n"
  },
  {
    "path": "examples/readme/select/scroll/scroll.tape",
    "content": "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 500ms\n  Enter\n  Ctrl+L\n  Sleep 500ms\n  Type \"clear && ./scroll\"\n  Sleep 500ms\n  Enter\n  Sleep 500ms\nShow\n\nDown@250ms 20\n\nHide\nType \"rm scroll\" Enter\n"
  },
  {
    "path": "examples/readme/select/select.tape",
    "content": "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  Enter\n  Ctrl+L\n  Sleep 500ms\n  Type \"clear && ./select\"\n  Sleep 500ms\n  Enter\n  Sleep 500ms\nShow\n\nSleep 1s\nDown@500ms 3\nSleep 1s\nUp@500ms 2\nSleep 1s\n\nType \"/\"\nSleep 1s\nType \"cana\"\nSleep 2s\n\nHide\nType \"rm select\" Enter\n"
  },
  {
    "path": "examples/readme/text/main.go",
    "content": "package main\n\nimport \"charm.land/huh/v2\"\n\n// TODO: ensure input is not plagiarized.\nfunc checkForPlagiarism(s string) error { return nil }\n\nfunc main() {\n\tvar story string\n\n\ttext := huh.NewText().\n\t\tTitle(\"Tell me a story.\").\n\t\tValidate(checkForPlagiarism).\n\t\tPlaceholder(\"What's on your mind?\").\n\t\tValue(&story)\n\n\t// Create a form to show help.\n\tform := huh.NewForm(huh.NewGroup(text))\n\tform.Run()\n}\n"
  },
  {
    "path": "examples/readme/text/text.tape",
    "content": "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  Type \"./text\" Enter\n  Sleep 500ms\nShow\n\nSleep 1s\nType \"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.\"\nSleep 4s\n\nHide\nType \"rm text\" Enter\n"
  },
  {
    "path": "examples/scroll/main.go",
    "content": "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(\"First\"),\n\t\t\thuh.NewInput().Title(\"Second\"),\n\t\t\thuh.NewInput().Title(\"Third\"),\n\t\t\thuh.NewInput().Title(\"Fourth\"),\n\t\t\thuh.NewInput().Title(\"Fifth\"),\n\t\t\thuh.NewInput().Title(\"Sixth\"),\n\t\t\thuh.NewInput().Title(\"Seventh\"),\n\t\t\thuh.NewInput().Title(\"Eighth\"),\n\t\t\thuh.NewInput().Title(\"Nineth\"),\n\t\t\thuh.NewInput().Title(\"Tenth\"),\n\t\t),\n\t).WithHeight(5)\n\tform.Run()\n}\n"
  },
  {
    "path": "examples/skip/main.go",
    "content": "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\tTitle(\"Charmburger\").\n\t\t\t\tDescription(\"Welcome to _Charmburger™_.\"),\n\n\t\t\thuh.NewSelect[string]().\n\t\t\t\tOptions(huh.NewOptions(\"Charmburger Classic\", \"Chickwich\", \"Fishburger\", \"Charmpossible™ Burger\")...).\n\t\t\t\tTitle(\"Choose your burger\").\n\t\t\t\tDescription(\"At Charm we truly have a burger for everyone.\"),\n\n\t\t\thuh.NewNote().\n\t\t\t\tTitle(\"🍔\"),\n\t\t),\n\n\t\thuh.NewGroup(\n\t\t\thuh.NewNote().\n\t\t\t\tTitle(\"Buy 1 get 1 free\").\n\t\t\t\tDescription(\"Welcome back to _Charmburger™_.\"),\n\n\t\t\thuh.NewSelect[string]().\n\t\t\t\tOptions(huh.NewOptions(\"Charmburger Classic\", \"Chickwich\", \"Fishburger\", \"Charmpossible™ Burger\")...).\n\t\t\t\tTitle(\"Choose your burger\").\n\t\t\t\tDescription(\"At Charm we truly have a burger for everyone.\"),\n\n\t\t\thuh.NewNote().\n\t\t\t\tTitle(\"🍔\"),\n\t\t),\n\t)\n\n\tf.Run()\n}\n"
  },
  {
    "path": "examples/spinner/accessible/main.go",
    "content": "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.WithTimeout(context.Background(), time.Second/2)\n\tdefer cancel()\n\n\terr := spinner.New().\n\t\tContext(ctx).\n\t\tWithAccessible(true).\n\t\tRun()\n\tif err != nil {\n\t\tlog.Fatalln(err)\n\t}\n}\n"
  },
  {
    "path": "examples/spinner/context/main.go",
    "content": "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.Sleep(5 * time.Second) }\n\tctx, cancel := context.WithTimeout(context.Background(), time.Second)\n\tdefer cancel()\n\tgo action()\n\tspinner.New().Context(ctx).Run()\n\tfmt.Println(\"Done!\")\n}\n"
  },
  {
    "path": "examples/spinner/context-and-action/main.go",
    "content": "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, cancel := context.WithTimeout(context.Background(), time.Second)\n\tdefer cancel()\n\n\terr := spinner.New().\n\t\tContext(ctx).\n\t\tAction(func() {\n\t\t\ttime.Sleep(time.Minute)\n\t\t}).\n\t\tWithAccessible(rand.Int()%2 == 0).\n\t\tRun()\n\tif err != nil {\n\t\tlog.Fatalln(err)\n\t}\n\tfmt.Println(\"Done!\")\n}\n"
  },
  {
    "path": "examples/spinner/context-and-action-and-error/main.go",
    "content": "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 := context.WithTimeout(context.Background(), time.Second)\n\tdefer cancel()\n\n\terr := spinner.New().\n\t\tContext(ctx).\n\t\tActionWithErr(func(context.Context) error {\n\t\t\ttime.Sleep(5 * time.Second)\n\t\t\treturn nil\n\t\t}).\n\t\tWithAccessible(false).\n\t\tRun()\n\tif err != nil {\n\t\tlog.Fatalln(err)\n\t}\n\tfmt.Println(\"Done!\")\n}\n"
  },
  {
    "path": "examples/spinner/loading/demo.tape",
    "content": "Output spinner.gif\n\nSet FontSize 32\nSet Height 225\nSet Width 800\n\nHide\nType \"go build -o spinner .\" Enter\nCtrl+L\nSleep 1s\nShow\n\nType \"./spinner\"\nSleep 500ms\nEnter\n\nSleep 4s\n"
  },
  {
    "path": "examples/spinner/loading/main.go",
    "content": "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\taction := func() {\n\t\ttime.Sleep(1 * time.Second)\n\t}\n\tif err := spinner.New().\n\t\tTitle(\"Preparing your burger...\").\n\t\tAction(action).\n\t\tWithViewHook(func(v tea.View) tea.View {\n\t\t\tv.ProgressBar = tea.NewProgressBar(tea.ProgressBarIndeterminate, 1)\n\t\t\treturn v\n\t\t}).\n\t\tRun(); err != nil {\n\t\tfmt.Println(\"Failed:\", err)\n\t\treturn\n\t}\n\tfmt.Println(\"Order up!\")\n}\n"
  },
  {
    "path": "examples/spinner/static/main.go",
    "content": "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\").WithAccessible(true).Run()\n\tfmt.Println(\"Done!\")\n}\n"
  },
  {
    "path": "examples/ssh-form/main.go",
    "content": "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/bubbletea/v2\"\n\t\"charm.land/huh/v2\"\n\t\"charm.land/lipgloss/v2\"\n\t\"charm.land/log/v2\"\n\t\"charm.land/wish/v2\"\n\t\"charm.land/wish/v2/activeterm\"\n\t\"charm.land/wish/v2/bubbletea\"\n\t\"github.com/charmbracelet/ssh\"\n)\n\nconst (\n\thost = \"localhost\"\n\tport = \"2222\"\n)\n\nfunc main() {\n\ts, err := wish.NewServer(\n\t\twish.WithAddress(net.JoinHostPort(host, port)),\n\t\twish.WithHostKeyPath(\".ssh/id_ed25519\"),\n\t\twish.WithMiddleware(\n\t\t\tbubbletea.Middleware(teaHandler),\n\t\t\tactiveterm.Middleware(),\n\t\t),\n\t)\n\tif err != nil {\n\t\tlog.Error(\"Could not start server\", \"error\", err)\n\t}\n\n\tdone := make(chan os.Signal, 1)\n\tsignal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)\n\tlog.SetReportTimestamp(false)\n\tlog.Infof(\"Running form over ssh, connect with:\")\n\tfmt.Printf(\"\\n  ssh %s -p %s\\n\\n\", host, port)\n\tgo func() {\n\t\tif err = s.ListenAndServe(); err != nil && !errors.Is(err, ssh.ErrServerClosed) {\n\t\t\tlog.Error(\"Could not start server\", \"error\", err)\n\t\t\tdone <- nil\n\t\t}\n\t}()\n\n\t<-done\n\tlog.Info(\"Stopping SSH server\")\n\tctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)\n\tdefer cancel()\n\tif err := s.Shutdown(ctx); err != nil && !errors.Is(err, ssh.ErrServerClosed) {\n\t\tlog.Error(\"Could not stop server\", \"error\", err)\n\t}\n}\n\nfunc customTheme(hasDarkBg bool) *huh.Styles {\n\tcustom := huh.ThemeBase(hasDarkBg)\n\tcustom.Blurred.Title = lipgloss.NewStyle().\n\t\tForeground(lipgloss.Color(\"#444\"))\n\tcustom.Blurred.TextInput.Prompt = lipgloss.NewStyle().\n\t\tForeground(lipgloss.Color(\"#444\"))\n\tcustom.Blurred.TextInput.Text = lipgloss.NewStyle().\n\t\tForeground(lipgloss.Color(\"#444\"))\n\tcustom.Focused.TextInput.Cursor = lipgloss.NewStyle().\n\t\tForeground(lipgloss.Color(\"#7571F9\"))\n\tcustom.Focused.Base = lipgloss.NewStyle().\n\t\tPadding(0, 1).\n\t\tBorder(lipgloss.ThickBorder(), false).\n\t\tBorderLeft(true).\n\t\tBorderForeground(lipgloss.Color(\"#7571F9\"))\n\treturn custom\n}\n\nfunc teaHandler(s ssh.Session) (tea.Model, []tea.ProgramOption) {\n\tform := huh.NewForm(\n\t\thuh.NewGroup(\n\t\t\thuh.NewInput().Title(\"Username\").Key(\"username\"),\n\t\t\thuh.NewInput().Title(\"Password\").EchoMode(huh.EchoModePassword),\n\t\t),\n\t).WithTheme(huh.ThemeFunc(customTheme))\n\tstyle := lipgloss.NewStyle().\n\t\tBorder(lipgloss.NormalBorder()).\n\t\tPadding(1, 2).\n\t\tBorderForeground(lipgloss.Color(\"#444444\")).\n\t\tForeground(lipgloss.Color(\"#7571F9\"))\n\tm := model{form: form, style: style}\n\treturn m, nil\n}\n\ntype model struct {\n\tform     *huh.Form\n\tstyle    lipgloss.Style\n\tloggedIn bool\n}\n\nfunc (m model) Init() tea.Cmd { return m.form.Init() }\n\nfunc (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {\n\tvar cmds []tea.Cmd\n\n\tif m.form != nil {\n\t\tf, cmd := m.form.Update(msg)\n\t\tm.form = f.(*huh.Form)\n\t\tcmds = append(cmds, cmd)\n\t}\n\n\tm.loggedIn = m.form.State == huh.StateCompleted\n\tif m.form.State == huh.StateAborted {\n\t\treturn m, tea.Quit\n\t}\n\n\tswitch msg := msg.(type) {\n\tcase tea.KeyPressMsg:\n\t\tswitch msg.String() {\n\t\tcase \"q\", \"ctrl+c\":\n\t\t\treturn m, tea.Quit\n\t\t}\n\t}\n\n\treturn m, tea.Batch(cmds...)\n}\n\nfunc (m model) View() tea.View {\n\tvar view tea.View\n\tview.AltScreen = true\n\n\tswitch {\n\tcase m.form == nil:\n\t\tview.SetContent(\"Starting...\")\n\tcase m.loggedIn:\n\t\tview.SetContent(m.style.Render(\"Welcome, \" + m.form.GetString(\"username\") + \"!\"))\n\tdefault:\n\t\tview.SetContent(m.form.View())\n\t}\n\treturn view\n}\n"
  },
  {
    "path": "examples/stickers/main.go",
    "content": "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\temail   string\n\t)\n\n\thuh.NewForm(\n\t\thuh.NewGroup(\n\t\t\thuh.NewNote().\n\t\t\t\tTitle(\"\\nStickers pls.\").\n\t\t\t\tDescription(\"Make sure to fill out the address exactly\\nas it would appear on a parcel.\"),\n\t\t\thuh.NewInput().\n\t\t\t\tTitle(\"Full name\").\n\t\t\t\tValidate(huh.ValidateMinLength(1)).\n\t\t\t\tValue(&name),\n\t\t\thuh.NewSelect[string]().\n\t\t\t\tTitle(\"Country \").\n\t\t\t\tHeight(1).\n\t\t\t\tValue(&country).\n\t\t\t\tInline(true).\n\t\t\t\tOptions(countries...),\n\t\t\thuh.NewText().\n\t\t\t\tTitle(\"Address\").\n\t\t\t\tLines(2).\n\t\t\t\tDescription(\"Use your country's postal format.\").\n\t\t\t\tValue(&address),\n\t\t\thuh.NewInput().\n\t\t\t\tTitle(\"Email\").\n\t\t\t\tDescription(\"Optional: so we can send you updates.\").\n\t\t\t\tValue(&email),\n\t\t),\n\t).Run()\n}\n\nvar countries = huh.NewOptions(\n\t// common\n\t\"United States\",\n\t\"Canada\",\n\t\"Germany\",\n\t\"Brazil\",\n\t\"Mexico\",\n\t\"China\",\n\t\"India\",\n\n\t\"Afghanistan\",\n\t\"Albania\",\n\t\"Algeria\",\n\t\"American Samoa\",\n\t\"Andorra\",\n\t\"Angola\",\n\t\"Anguilla\",\n\t\"Antarctica\",\n\t\"Antigua and Barbuda\",\n\t\"Argentina\",\n\t\"Armenia\",\n\t\"Aruba\",\n\t\"Australia\",\n\t\"Austria\",\n\t\"Azerbaijan\",\n\t\"Ã…land Islands\",\n\t\"Bahamas\",\n\t\"Bahrain\",\n\t\"Bangladesh\",\n\t\"Barbados\",\n\t\"Belarus\",\n\t\"Belgium\",\n\t\"Belize\",\n\t\"Benin\",\n\t\"Bermuda\",\n\t\"Bhutan\",\n\t\"Bolivia\",\n\t\"Bosnia and Herzegovina\",\n\t\"Botswana\",\n\t\"Bouvet Island\",\n\t\"British Indian Ocean Territory\",\n\t\"British Virgin Islands\",\n\t\"Brunei\",\n\t\"Bulgaria\",\n\t\"Burkina Faso\",\n\t\"Burundi\",\n\t\"Cambodia\",\n\t\"Cameroon\",\n\t\"Cape Verde\",\n\t\"Caribbean Netherlands\",\n\t\"Cayman Islands\",\n\t\"Central African Republic\",\n\t\"Chad\",\n\t\"Chile\",\n\t\"Christmas Island\",\n\t\"Cocos (Keeling) Islands\",\n\t\"Colombia\",\n\t\"Comoros\",\n\t\"Cook Islands\",\n\t\"Costa Rica\",\n\t\"Croatia\",\n\t\"Cuba\",\n\t\"CuraÃ§ao\",\n\t\"Cyprus\",\n\t\"Czechia\",\n\t\"DR Congo\",\n\t\"Denmark\",\n\t\"Djibouti\",\n\t\"Dominica\",\n\t\"Dominican Republic\",\n\t\"Ecuador\",\n\t\"Egypt\",\n\t\"El Salvador\",\n\t\"Equatorial Guinea\",\n\t\"Eritrea\",\n\t\"Estonia\",\n\t\"Eswatini\",\n\t\"Ethiopia\",\n\t\"Falkland Islands\",\n\t\"Faroe Islands\",\n\t\"Fiji\",\n\t\"Finland\",\n\t\"France\",\n\t\"French Guiana\",\n\t\"French Polynesia\",\n\t\"French Southern and Antarctic Lands\",\n\t\"Gabon\",\n\t\"Gambia\",\n\t\"Georgia\",\n\t\"Ghana\",\n\t\"Gibraltar\",\n\t\"Greece\",\n\t\"Greenland\",\n\t\"Grenada\",\n\t\"Guadeloupe\",\n\t\"Guam\",\n\t\"Guatemala\",\n\t\"Guernsey\",\n\t\"Guinea\",\n\t\"Guinea-Bissau\",\n\t\"Guyana\",\n\t\"Haiti\",\n\t\"Heard Island and McDonald Islands\",\n\t\"Honduras\",\n\t\"Hong Kong\",\n\t\"Hungary\",\n\t\"Iceland\",\n\t\"Indonesia\",\n\t\"Iran\",\n\t\"Iraq\",\n\t\"Ireland\",\n\t\"Isle of Man\",\n\t\"Israel\",\n\t\"Italy\",\n\t\"Ivory Coast\",\n\t\"Jamaica\",\n\t\"Japan\",\n\t\"Jersey\",\n\t\"Jordan\",\n\t\"Kazakhstan\",\n\t\"Kenya\",\n\t\"Kiribati\",\n\t\"Kosovo\",\n\t\"Kuwait\",\n\t\"Kyrgyzstan\",\n\t\"Laos\",\n\t\"Latvia\",\n\t\"Lebanon\",\n\t\"Lesotho\",\n\t\"Liberia\",\n\t\"Libya\",\n\t\"Liechtenstein\",\n\t\"Lithuania\",\n\t\"Luxembourg\",\n\t\"Macau\",\n\t\"Madagascar\",\n\t\"Malawi\",\n\t\"Malaysia\",\n\t\"Maldives\",\n\t\"Mali\",\n\t\"Malta\",\n\t\"Marshall Islands\",\n\t\"Martinique\",\n\t\"Mauritania\",\n\t\"Mauritius\",\n\t\"Mayotte\",\n\t\"Micronesia\",\n\t\"Moldova\",\n\t\"Monaco\",\n\t\"Mongolia\",\n\t\"Montenegro\",\n\t\"Montserrat\",\n\t\"Morocco\",\n\t\"Mozambique\",\n\t\"Myanmar\",\n\t\"Namibia\",\n\t\"Nauru\",\n\t\"Nepal\",\n\t\"Netherlands\",\n\t\"New Caledonia\",\n\t\"New Zealand\",\n\t\"Nicaragua\",\n\t\"Niger\",\n\t\"Nigeria\",\n\t\"Niue\",\n\t\"Norfolk Island\",\n\t\"North Korea\",\n\t\"North Macedonia\",\n\t\"Northern Mariana Islands\",\n\t\"Norway\",\n\t\"Oman\",\n\t\"Pakistan\",\n\t\"Palau\",\n\t\"Palestine\",\n\t\"Panama\",\n\t\"Papua New Guinea\",\n\t\"Paraguay\",\n\t\"Peru\",\n\t\"Philippines\",\n\t\"Pitcairn Islands\",\n\t\"Poland\",\n\t\"Portugal\",\n\t\"Puerto Rico\",\n\t\"Qatar\",\n\t\"Republic of the Congo\",\n\t\"Romania\",\n\t\"Russia\",\n\t\"Rwanda\",\n\t\"SÃ£o TomÃ© and PrÃ­ncipe\",\n\t\"Saint BarthÃ©lemy\",\n\t\"Saint Helena, Ascension and Tristan da Cunha\",\n\t\"Saint Kitts and Nevis\",\n\t\"Saint Lucia\",\n\t\"Saint Martin\",\n\t\"Saint Pierre and Miquelon\",\n\t\"Saint Vincent and the Grenadines\",\n\t\"Samoa\",\n\t\"San Marino\",\n\t\"Saudi Arabia\",\n\t\"Senegal\",\n\t\"Serbia\",\n\t\"Seychelles\",\n\t\"Sierra Leone\",\n\t\"Singapore\",\n\t\"Sint Maarten\",\n\t\"Slovakia\",\n\t\"Slovenia\",\n\t\"Solomon Islands\",\n\t\"Somalia\",\n\t\"South Africa\",\n\t\"South Georgia\",\n\t\"South Korea\",\n\t\"South Sudan\",\n\t\"Spain\",\n\t\"Sri Lanka\",\n\t\"Sudan\",\n\t\"Suriname\",\n\t\"Svalbard and Jan Mayen\",\n\t\"Sweden\",\n\t\"Switzerland\",\n\t\"Syria\",\n\t\"Taiwan\",\n\t\"Tajikistan\",\n\t\"Tanzania\",\n\t\"Thailand\",\n\t\"Timor-Leste\",\n\t\"Togo\",\n\t\"Tokelau\",\n\t\"Tonga\",\n\t\"Trinidad and Tobago\",\n\t\"Tunisia\",\n\t\"Turkey\",\n\t\"Turkmenistan\",\n\t\"Turks and Caicos Islands\",\n\t\"Tuvalu\",\n\t\"Uganda\",\n\t\"Ukraine\",\n\t\"United Arab Emirates\",\n\t\"United Kingdom\",\n\t\"United States Minor Outlying Islands\",\n\t\"United States Virgin Islands\",\n\t\"Uruguay\",\n\t\"Uzbekistan\",\n\t\"Vanuatu\",\n\t\"Vatican City\",\n\t\"Venezuela\",\n\t\"Vietnam\",\n\t\"Wallis and Futuna\",\n\t\"Western Sahara\",\n\t\"Yemen\",\n\t\"Zambia\",\n\t\"Zimbabwe\",\n)\n"
  },
  {
    "path": "examples/theme/main.go",
    "content": "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.ThemeFunc(huh.ThemeBase),\n\t\"dracula\":    huh.ThemeFunc(huh.ThemeDracula),\n\t\"base16\":     huh.ThemeFunc(huh.ThemeBase16),\n\t\"charm\":      huh.ThemeFunc(huh.ThemeCharm),\n\t\"catppuccin\": huh.ThemeFunc(huh.ThemeCatppuccin),\n}\n\nfunc main() {\n\ttheme := \"base16\"\n\trepeat := true\n\n\tfor {\n\t\terr := huh.NewSelect[string]().\n\t\t\tTitle(\"Theme\").\n\t\t\tValue(&theme).\n\t\t\tOptions(\n\t\t\t\thuh.NewOption(\"Default\", \"default\"),\n\t\t\t\thuh.NewOption(\"Dracula\", \"dracula\"),\n\t\t\t\thuh.NewOption(\"Base 16\", \"base16\"),\n\t\t\t\thuh.NewOption(\"Charm\", \"charm\"),\n\t\t\t\thuh.NewOption(\"Catppuccin\", \"catppuccin\"),\n\t\t\t\thuh.NewOption(\"Exit\", \"\"),\n\t\t\t).Run()\n\t\tif err != nil {\n\t\t\tif err == huh.ErrUserAborted {\n\t\t\t\tos.Exit(130)\n\t\t\t}\n\t\t\tfmt.Println(err)\n\t\t\tos.Exit(1)\n\t\t}\n\t\tif theme == \"\" {\n\t\t\tbreak\n\t\t}\n\n\t\t// Display form with selected theme.\n\t\terr = huh.NewForm(\n\t\t\thuh.NewGroup(\n\t\t\t\thuh.NewInput().Title(\"Thoughts\").Placeholder(\"What's on your mind?\"),\n\t\t\t\thuh.NewSelect[string]().Options(huh.NewOptions(\"A\", \"B\", \"C\")...).Title(\"Colors\"),\n\t\t\t\thuh.NewFilePicker().Title(\"File\"),\n\t\t\t\thuh.NewMultiSelect[string]().Options(huh.NewOptions(\"Red\", \"Green\", \"Yellow\")...).Title(\"Letters\"),\n\t\t\t\thuh.NewConfirm().Title(\"Again?\").Description(\"Try another theme\").Value(&repeat),\n\t\t\t),\n\t\t).WithTheme(themes[theme]).Run()\n\t\tif err != nil {\n\t\t\tif err == huh.ErrUserAborted {\n\t\t\t\tos.Exit(130)\n\t\t\t}\n\t\t\tfmt.Println(err)\n\t\t\tos.Exit(1)\n\t\t}\n\n\t\tif !repeat {\n\t\t\tbreak\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "examples/theme/theme.tape",
    "content": "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\nType \"clear && ./theme\" Enter\nShow\n\nSleep 2s\n\nEnter\n\nScreenshot default-theme.png\n\nTab 4\nDown 1\nEnter\n\nScreenshot dracula-theme.png\n\nTab 4\nDown 2\nEnter\n\nScreenshot basesixteen-theme.png\n\nTab 4\nDown 3\nEnter\n\nScreenshot charm-theme.png\n\nTab 4\nDown 4\nEnter\n\nScreenshot catppuccin-theme.png\n\nSleep 1s\n\nHide\nType \"rm theme\"\nEnter\n"
  },
  {
    "path": "examples/timer/main.go",
    "content": "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\"charm.land/huh/v2\"\n\t\"charm.land/lipgloss/v2\"\n)\n\nvar (\n\tfocusColor = lipgloss.Color(\"#2EF8BB\")\n\tbreakColor = lipgloss.Color(\"#FF5F87\")\n)\n\nvar (\n\tfocusTitleStyle = lipgloss.NewStyle().Foreground(focusColor).MarginRight(1).SetString(\"Focus Mode\")\n\tbreakTitleStyle = lipgloss.NewStyle().Foreground(breakColor).MarginRight(1).SetString(\"Break Mode\")\n\tpausedStyle     = lipgloss.NewStyle().Foreground(breakColor).MarginRight(1).SetString(\"Continue?\")\n\thelpStyle       = lipgloss.NewStyle().Foreground(lipgloss.Color(\"240\")).MarginTop(2)\n\tsidebarStyle    = lipgloss.NewStyle().MarginLeft(3).Padding(1, 3).Border(lipgloss.RoundedBorder()).BorderForeground(helpStyle.GetForeground())\n)\n\nvar baseTimerStyle = lipgloss.NewStyle().Padding(1, 2)\n\ntype mode int\n\nconst (\n\tInitial mode = iota\n\tFocusing\n\tPaused\n\tBreaking\n)\n\ntype Model struct {\n\tform     *huh.Form\n\tquitting bool\n\n\tlastTick  time.Time\n\tstartTime time.Time\n\n\tmode mode\n\n\tfocusTime time.Duration\n\tbreakTime time.Duration\n\n\tprogress progress.Model\n}\n\nfunc (m Model) Init() tea.Cmd {\n\treturn m.form.Init()\n}\n\nconst tickInterval = time.Second / 2\n\ntype tickMsg time.Time\n\nfunc tickCmd(t time.Time) tea.Msg {\n\treturn tickMsg(t)\n}\n\nfunc (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {\n\tvar cmds []tea.Cmd\n\n\tswitch msg := msg.(type) {\n\tcase tickMsg:\n\t\tcmds = append(cmds, tea.Tick(tickInterval, tickCmd))\n\tcase tea.KeyPressMsg:\n\t\tswitch msg.String() {\n\t\tcase \"q\":\n\t\t\tswitch m.mode {\n\t\t\tcase Focusing:\n\t\t\t\tm.mode = Paused\n\t\t\t\tm.startTime = time.Now()\n\t\t\t\tm.progress.FullColor = breakColor\n\t\t\tcase Paused:\n\t\t\t\tm.mode = Breaking\n\t\t\t\tm.startTime = time.Now()\n\t\t\tcase Breaking:\n\t\t\t\tm.quitting = true\n\t\t\t\treturn m, tea.Quit\n\t\t\t}\n\t\tcase \"ctrl+c\":\n\t\t\tm.quitting = true\n\t\t\treturn m, tea.Interrupt\n\t\tdefault:\n\t\t\tif m.mode == Paused {\n\t\t\t\tm.mode = Breaking\n\t\t\t\tm.startTime = time.Now()\n\t\t\t}\n\t\t}\n\t}\n\n\t// Update form\n\tf, cmd := m.form.Update(msg)\n\tm.form = f.(*huh.Form)\n\tcmds = append(cmds, cmd)\n\tif m.form.State != huh.StateCompleted {\n\t\treturn m, tea.Batch(cmds...)\n\t}\n\n\t// Update timer\n\tif m.startTime.IsZero() {\n\t\tm.startTime = time.Now()\n\t\tm.focusTime = m.form.Get(\"focus\").(time.Duration)\n\t\tm.breakTime = m.form.Get(\"break\").(time.Duration)\n\t\tm.mode = Focusing\n\t\tcmds = append(cmds, tea.Tick(tickInterval, tickCmd))\n\t}\n\n\tswitch m.mode {\n\tcase Focusing:\n\t\tif time.Now().After(m.startTime.Add(m.focusTime)) {\n\t\t\tm.mode = Paused\n\t\t\tm.startTime = time.Now()\n\t\t\tm.progress.FullColor = breakColor\n\t\t}\n\tcase Breaking:\n\t\tif time.Now().After(m.startTime.Add(m.breakTime)) {\n\t\t\tm.quitting = true\n\t\t\treturn m, tea.Quit\n\t\t}\n\t}\n\n\treturn m, tea.Batch(cmds...)\n}\n\nfunc (m Model) View() tea.View {\n\tif m.quitting {\n\t\treturn tea.NewView(\"\")\n\t}\n\n\tif m.form.State != huh.StateCompleted {\n\t\treturn tea.NewView(m.form.View())\n\t}\n\n\tvar s strings.Builder\n\n\telapsed := time.Since(m.startTime)\n\tvar percent float64\n\tswitch m.mode {\n\tcase Focusing:\n\t\tpercent = float64(elapsed) / float64(m.focusTime)\n\t\ts.WriteString(focusTitleStyle.String())\n\t\ts.WriteString(elapsed.Round(time.Second).String())\n\t\ts.WriteString(\"\\n\\n\")\n\t\ts.WriteString(m.progress.ViewAs(percent))\n\t\ts.WriteString(helpStyle.Render(\"Press 'q' to skip\"))\n\tcase Paused:\n\t\ts.WriteString(pausedStyle.String())\n\t\ts.WriteString(\"\\n\\nFocus time is done, time to take a break.\")\n\t\ts.WriteString(helpStyle.Render(\"press any key to continue.\\n\"))\n\tcase Breaking:\n\t\tpercent = float64(elapsed) / float64(m.breakTime)\n\t\ts.WriteString(breakTitleStyle.String())\n\t\ts.WriteString(elapsed.Round(time.Second).String())\n\t\ts.WriteString(\"\\n\\n\")\n\t\ts.WriteString(m.progress.ViewAs(percent))\n\t\ts.WriteString(helpStyle.Render(\"press 'q' to quit\"))\n\t}\n\n\treturn tea.NewView(baseTimerStyle.Render(s.String()))\n}\n\nfunc customTheme(isDark bool) *huh.Styles {\n\ttheme := huh.ThemeCharm(isDark)\n\ttheme.Focused.Base.Border(lipgloss.HiddenBorder())\n\ttheme.Focused.Title.Foreground(focusColor)\n\ttheme.Focused.SelectSelector.Foreground(focusColor)\n\ttheme.Focused.SelectedOption.Foreground(lipgloss.Color(\"15\"))\n\ttheme.Focused.Option.Foreground(lipgloss.Color(\"7\"))\n\treturn theme\n}\n\nfunc NewModel() Model {\n\tform := huh.NewForm(\n\t\thuh.NewGroup(\n\t\t\thuh.NewSelect[time.Duration]().\n\t\t\t\tTitle(\"Focus Time\").\n\t\t\t\tKey(\"focus\").\n\t\t\t\tOptions(\n\t\t\t\t\thuh.NewOption(\"25 minutes\", 25*time.Minute),\n\t\t\t\t\thuh.NewOption(\"30 minutes\", 30*time.Minute),\n\t\t\t\t\thuh.NewOption(\"45 minutes\", 45*time.Minute),\n\t\t\t\t\thuh.NewOption(\"1 hour\", time.Hour),\n\t\t\t\t),\n\t\t),\n\t\thuh.NewGroup(\n\t\t\thuh.NewSelect[time.Duration]().\n\t\t\t\tTitle(\"Break Time\").\n\t\t\t\tKey(\"break\").\n\t\t\t\tOptions(\n\t\t\t\t\thuh.NewOption(\"5 minutes\", 5*time.Minute),\n\t\t\t\t\thuh.NewOption(\"10 minutes\", 10*time.Minute),\n\t\t\t\t\thuh.NewOption(\"15 minutes\", 15*time.Minute),\n\t\t\t\t\thuh.NewOption(\"20 minutes\", 20*time.Minute),\n\t\t\t\t),\n\t\t),\n\t).WithShowHelp(false).WithTheme(huh.ThemeFunc(customTheme))\n\n\tprogress := progress.New()\n\tprogress.FullColor = focusColor\n\tprogress.SetSpringOptions(1, 1)\n\n\treturn Model{\n\t\tform:     form,\n\t\tprogress: progress,\n\t}\n}\n\nfunc main() {\n\tm := NewModel()\n\tmm, err := tea.NewProgram(&m).Run()\n\tm = mm.(Model)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n"
  },
  {
    "path": "field_confirm.go",
    "content": "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/huh/v2/internal/accessibility\"\n\t\"charm.land/lipgloss/v2\"\n)\n\n// Confirm is a form confirm field.\ntype Confirm struct {\n\taccessor Accessor[bool]\n\tkey      string\n\tid       int\n\n\t// customization\n\ttitle       Eval[string]\n\tdescription Eval[string]\n\taffirmative string\n\tnegative    string\n\n\t// error handling\n\tvalidate func(bool) error\n\terr      error\n\n\t// state\n\tfocused bool\n\n\t// options\n\twidth           int\n\theight          int\n\tinline          bool\n\ttheme           Theme\n\thasDarkBg       bool\n\tkeymap          ConfirmKeyMap\n\tbuttonAlignment lipgloss.Position\n}\n\n// NewConfirm returns a new confirm field.\nfunc NewConfirm() *Confirm {\n\treturn &Confirm{\n\t\taccessor:        &EmbeddedAccessor[bool]{},\n\t\tid:              nextID(),\n\t\ttitle:           Eval[string]{cache: make(map[uint64]string)},\n\t\tdescription:     Eval[string]{cache: make(map[uint64]string)},\n\t\taffirmative:     \"Yes\",\n\t\tnegative:        \"No\",\n\t\tvalidate:        func(bool) error { return nil },\n\t\tbuttonAlignment: lipgloss.Center,\n\t}\n}\n\n// Validate sets the validation function of the confirm field.\nfunc (c *Confirm) Validate(validate func(bool) error) *Confirm {\n\tc.validate = validate\n\treturn c\n}\n\n// Error returns the error of the confirm field.\nfunc (c *Confirm) Error() error {\n\treturn c.err\n}\n\n// Skip returns whether the confirm should be skipped or should be blocking.\nfunc (*Confirm) Skip() bool {\n\treturn false\n}\n\n// Zoom returns whether the input should be zoomed.\nfunc (*Confirm) Zoom() bool {\n\treturn false\n}\n\n// Affirmative sets the affirmative value of the confirm field.\nfunc (c *Confirm) Affirmative(affirmative string) *Confirm {\n\tc.affirmative = affirmative\n\treturn c\n}\n\n// Negative sets the negative value of the confirm field.\nfunc (c *Confirm) Negative(negative string) *Confirm {\n\tc.negative = negative\n\treturn c\n}\n\n// Value sets the value of the confirm field.\nfunc (c *Confirm) Value(value *bool) *Confirm {\n\treturn c.Accessor(NewPointerAccessor(value))\n}\n\n// Accessor sets the accessor of the confirm field.\nfunc (c *Confirm) Accessor(accessor Accessor[bool]) *Confirm {\n\tc.accessor = accessor\n\treturn c\n}\n\n// Key sets the key of the confirm field.\nfunc (c *Confirm) Key(key string) *Confirm {\n\tc.key = key\n\treturn c\n}\n\n// Title sets the title of the confirm field.\nfunc (c *Confirm) Title(title string) *Confirm {\n\tc.title.val = title\n\tc.title.fn = nil\n\treturn c\n}\n\n// TitleFunc sets the title func of the confirm field.\nfunc (c *Confirm) TitleFunc(f func() string, bindings any) *Confirm {\n\tc.title.fn = f\n\tc.title.bindings = bindings\n\treturn c\n}\n\n// Description sets the description of the confirm field.\nfunc (c *Confirm) Description(description string) *Confirm {\n\tc.description.val = description\n\tc.description.fn = nil\n\treturn c\n}\n\n// DescriptionFunc sets the description function of the confirm field.\nfunc (c *Confirm) DescriptionFunc(f func() string, bindings any) *Confirm {\n\tc.description.fn = f\n\tc.description.bindings = bindings\n\treturn c\n}\n\n// Inline sets whether the field should be inline.\nfunc (c *Confirm) Inline(inline bool) *Confirm {\n\tc.inline = inline\n\treturn c\n}\n\n// Focus focuses the confirm field.\nfunc (c *Confirm) Focus() tea.Cmd {\n\tc.focused = true\n\treturn nil\n}\n\n// Blur blurs the confirm field.\nfunc (c *Confirm) Blur() tea.Cmd {\n\tc.focused = false\n\tc.err = c.validate(c.accessor.Get())\n\treturn nil\n}\n\n// KeyBinds returns the help message for the confirm field.\nfunc (c *Confirm) KeyBinds() []key.Binding {\n\treturn []key.Binding{c.keymap.Toggle, c.keymap.Prev, c.keymap.Submit, c.keymap.Next, c.keymap.Accept, c.keymap.Reject}\n}\n\n// Init initializes the confirm field.\nfunc (c *Confirm) Init() tea.Cmd {\n\treturn nil\n}\n\n// Update updates the confirm field.\nfunc (c *Confirm) Update(msg tea.Msg) (Model, tea.Cmd) {\n\tvar cmds []tea.Cmd\n\n\tswitch msg := msg.(type) {\n\tcase tea.BackgroundColorMsg:\n\t\tc.hasDarkBg = msg.IsDark()\n\tcase updateFieldMsg:\n\t\tif ok, hash := c.title.shouldUpdate(); ok {\n\t\t\tc.title.bindingsHash = hash\n\t\t\tif !c.title.loadFromCache() {\n\t\t\t\tc.title.loading = true\n\t\t\t\tcmds = append(cmds, func() tea.Msg {\n\t\t\t\t\treturn updateTitleMsg{id: c.id, title: c.title.fn(), hash: hash}\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t\tif ok, hash := c.description.shouldUpdate(); ok {\n\t\t\tc.description.bindingsHash = hash\n\t\t\tif !c.description.loadFromCache() {\n\t\t\t\tc.description.loading = true\n\t\t\t\tcmds = append(cmds, func() tea.Msg {\n\t\t\t\t\treturn updateDescriptionMsg{id: c.id, description: c.description.fn(), hash: hash}\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\tcase updateTitleMsg:\n\t\tif msg.id == c.id && msg.hash == c.title.bindingsHash {\n\t\t\tc.title.val = msg.title\n\t\t\tc.title.loading = false\n\t\t}\n\tcase updateDescriptionMsg:\n\t\tif msg.id == c.id && msg.hash == c.description.bindingsHash {\n\t\t\tc.description.val = msg.description\n\t\t\tc.description.loading = false\n\t\t}\n\tcase tea.KeyPressMsg:\n\t\tc.err = nil\n\t\tswitch {\n\t\tcase key.Matches(msg, c.keymap.Toggle):\n\t\t\tif c.negative == \"\" {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tc.accessor.Set(!c.accessor.Get())\n\t\tcase key.Matches(msg, c.keymap.Prev):\n\t\t\tcmds = append(cmds, PrevField)\n\t\tcase key.Matches(msg, c.keymap.Next, c.keymap.Submit):\n\t\t\tcmds = append(cmds, NextField)\n\t\tcase key.Matches(msg, c.keymap.Accept):\n\t\t\tc.accessor.Set(true)\n\t\t\tcmds = append(cmds, NextField)\n\t\tcase key.Matches(msg, c.keymap.Reject):\n\t\t\tc.accessor.Set(false)\n\t\t\tcmds = append(cmds, NextField)\n\t\t}\n\t}\n\n\treturn c, tea.Batch(cmds...)\n}\n\nfunc (c *Confirm) activeStyles() *FieldStyles {\n\ttheme := c.theme\n\tif theme == nil {\n\t\ttheme = ThemeFunc(ThemeCharm)\n\t}\n\tif c.focused {\n\t\treturn &theme.Theme(c.hasDarkBg).Focused\n\t}\n\treturn &theme.Theme(c.hasDarkBg).Blurred\n}\n\n// View renders the confirm field.\nfunc (c *Confirm) View() string {\n\tstyles := c.activeStyles()\n\tmaxWidth := c.width - styles.Base.GetHorizontalFrameSize()\n\n\tvar wroteHeader bool\n\tvar sb strings.Builder\n\tif c.title.val != \"\" {\n\t\tsb.WriteString(styles.Title.Render(wrap(c.title.val, maxWidth)))\n\t\twroteHeader = true\n\t}\n\tif c.err != nil {\n\t\tsb.WriteString(styles.ErrorIndicator.String())\n\t\twroteHeader = true\n\t}\n\n\tif c.description.val != \"\" {\n\t\tdescription := styles.Description.Render(wrap(c.description.val, maxWidth))\n\t\tif !c.inline && (c.description.val != \"\" || c.description.fn != nil) {\n\t\t\tsb.WriteString(\"\\n\")\n\t\t}\n\t\tsb.WriteString(description)\n\t\twroteHeader = true\n\t}\n\n\tif !c.inline && wroteHeader {\n\t\tsb.WriteString(\"\\n\")\n\t\tsb.WriteString(\"\\n\")\n\t}\n\n\tvar negative string\n\tvar affirmative string\n\tif c.negative != \"\" {\n\t\tif c.accessor.Get() {\n\t\t\taffirmative = styles.FocusedButton.Render(c.affirmative)\n\t\t\tnegative = styles.BlurredButton.Render(c.negative)\n\t\t} else {\n\t\t\taffirmative = styles.BlurredButton.Render(c.affirmative)\n\t\t\tnegative = styles.FocusedButton.Render(c.negative)\n\t\t}\n\t\tc.keymap.Reject.SetHelp(\"n\", c.negative)\n\t} else {\n\t\taffirmative = styles.FocusedButton.Render(c.affirmative)\n\t\tc.keymap.Reject.SetEnabled(false)\n\t}\n\n\tc.keymap.Accept.SetHelp(\"y\", c.affirmative)\n\n\tbuttonsRow := lipgloss.JoinHorizontal(c.buttonAlignment, affirmative, negative)\n\n\tpromptWidth := lipgloss.Width(sb.String())\n\tbuttonsWidth := lipgloss.Width(buttonsRow)\n\n\trenderWidth := max(buttonsWidth, promptWidth)\n\n\tstyle := lipgloss.NewStyle().Width(renderWidth).Align(c.buttonAlignment)\n\n\tsb.WriteString(style.Render(buttonsRow))\n\treturn styles.Base.Width(c.width).Height(c.height).\n\t\tRender(sb.String())\n}\n\n// Run runs the confirm field in accessible mode.\nfunc (c *Confirm) Run() error {\n\treturn Run(c)\n}\n\n// RunAccessible runs the confirm field in accessible mode.\nfunc (c *Confirm) RunAccessible(w io.Writer, r io.Reader) error {\n\tstyles := c.activeStyles()\n\tdefaultValue := c.GetValue().(bool)\n\topts := \"[y/N]\"\n\tif defaultValue {\n\t\topts = \"[Y/n]\"\n\t}\n\tprompt := styles.Title.\n\t\tPaddingRight(1).\n\t\tRender(cmp.Or(c.title.val, \"Choose\"), opts)\n\tc.accessor.Set(accessibility.PromptBool(w, r, prompt, defaultValue))\n\treturn nil\n}\n\nfunc (c *Confirm) String() string {\n\tif c.accessor.Get() {\n\t\treturn c.affirmative\n\t}\n\treturn c.negative\n}\n\n// WithTheme sets the theme of the confirm field.\nfunc (c *Confirm) WithTheme(theme Theme) Field {\n\tif c.theme != nil {\n\t\treturn c\n\t}\n\tc.theme = theme\n\treturn c\n}\n\n// WithKeyMap sets the keymap of the confirm field.\nfunc (c *Confirm) WithKeyMap(k *KeyMap) Field {\n\tc.keymap = k.Confirm\n\treturn c\n}\n\n// WithWidth sets the width of the confirm field.\nfunc (c *Confirm) WithWidth(width int) Field {\n\tc.width = width\n\treturn c\n}\n\n// WithHeight sets the height of the confirm field.\nfunc (c *Confirm) WithHeight(height int) Field {\n\tc.height = height\n\treturn c\n}\n\n// WithPosition sets the position of the confirm field.\nfunc (c *Confirm) WithPosition(p FieldPosition) Field {\n\tc.keymap.Prev.SetEnabled(!p.IsFirst())\n\tc.keymap.Next.SetEnabled(!p.IsLast())\n\tc.keymap.Submit.SetEnabled(p.IsLast())\n\treturn c\n}\n\n// WithButtonAlignment sets the button position of the confirm field.\nfunc (c *Confirm) WithButtonAlignment(p lipgloss.Position) *Confirm {\n\tc.buttonAlignment = p\n\treturn c\n}\n\n// GetKey returns the key of the field.\nfunc (c *Confirm) GetKey() string {\n\treturn c.key\n}\n\n// GetValue returns the value of the field.\nfunc (c *Confirm) GetValue() any {\n\treturn c.accessor.Get()\n}\n"
  },
  {
    "path": "field_filepicker.go",
    "content": "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\"charm.land/bubbles/v2/filepicker\"\n\t\"charm.land/bubbles/v2/key\"\n\ttea \"charm.land/bubbletea/v2\"\n\t\"charm.land/huh/v2/internal/accessibility\"\n\t\"charm.land/lipgloss/v2\"\n)\n\n// FilePicker is a form file file field.\ntype FilePicker struct {\n\taccessor Accessor[string]\n\tkey      string\n\tpicker   filepicker.Model\n\n\t// state\n\tfocused bool\n\tpicking bool\n\n\t// customization\n\ttitle       string\n\tdescription string\n\n\t// error handling\n\tvalidate func(string) error\n\terr      error\n\n\t// options\n\twidth     int\n\theight    int\n\ttheme     Theme\n\thasDarkBg bool\n\tkeymap    FilePickerKeyMap\n}\n\n// NewFilePicker returns a new file field.\nfunc NewFilePicker() *FilePicker {\n\tfp := filepicker.New()\n\tfp.ShowSize = false\n\n\tif cmd := fp.Init(); cmd != nil {\n\t\tfp, _ = fp.Update(cmd())\n\t}\n\n\treturn &FilePicker{\n\t\taccessor: &EmbeddedAccessor[string]{},\n\t\tvalidate: func(string) error { return nil },\n\t\tpicker:   fp,\n\t}\n}\n\n// CurrentDirectory sets the directory of the file field.\nfunc (f *FilePicker) CurrentDirectory(directory string) *FilePicker {\n\tf.picker.CurrentDirectory = directory\n\tif cmd := f.picker.Init(); cmd != nil {\n\t\tf.picker, _ = f.picker.Update(cmd())\n\t}\n\treturn f\n}\n\n// Cursor sets the cursor of the file field.\nfunc (f *FilePicker) Cursor(cursor string) *FilePicker {\n\tf.picker.Cursor = cursor\n\treturn f\n}\n\n// Picking sets whether the file picker should be in the picking files state.\nfunc (f *FilePicker) Picking(v bool) *FilePicker {\n\tf.setPicking(v)\n\treturn f\n}\n\n// ShowHidden sets whether to show hidden files.\nfunc (f *FilePicker) ShowHidden(v bool) *FilePicker {\n\tf.picker.ShowHidden = v\n\treturn f\n}\n\n// ShowSize sets whether to show file sizes.\nfunc (f *FilePicker) ShowSize(v bool) *FilePicker {\n\tf.picker.ShowSize = v\n\treturn f\n}\n\n// ShowPermissions sets whether to show file permissions.\nfunc (f *FilePicker) ShowPermissions(v bool) *FilePicker {\n\tf.picker.ShowPermissions = v\n\treturn f\n}\n\n// FileAllowed sets whether to allow files to be selected.\nfunc (f *FilePicker) FileAllowed(v bool) *FilePicker {\n\tf.picker.FileAllowed = v\n\treturn f\n}\n\n// DirAllowed sets whether to allow directories to be selected.\nfunc (f *FilePicker) DirAllowed(v bool) *FilePicker {\n\tf.picker.DirAllowed = v\n\treturn f\n}\n\n// Value sets the value of the file field.\nfunc (f *FilePicker) Value(value *string) *FilePicker {\n\treturn f.Accessor(NewPointerAccessor(value))\n}\n\n// Accessor sets the accessor of the file field.\nfunc (f *FilePicker) Accessor(accessor Accessor[string]) *FilePicker {\n\tf.accessor = accessor\n\treturn f\n}\n\n// Key sets the key of the file field which can be used to retrieve the value\n// after submission.\nfunc (f *FilePicker) Key(key string) *FilePicker {\n\tf.key = key\n\treturn f\n}\n\n// Title sets the title of the file field.\nfunc (f *FilePicker) Title(title string) *FilePicker {\n\tf.title = title\n\treturn f\n}\n\n// Description sets the description of the file field.\nfunc (f *FilePicker) Description(description string) *FilePicker {\n\tf.description = description\n\treturn f\n}\n\n// AllowedTypes sets the allowed types of the file field. These will be the only\n// valid file types accepted, other files will show as disabled.\nfunc (f *FilePicker) AllowedTypes(types []string) *FilePicker {\n\tf.picker.AllowedTypes = types\n\treturn f\n}\n\n// Height sets the height of the file field. If the number of options\n// exceeds the height, the file field will become scrollable.\nfunc (f *FilePicker) Height(height int) *FilePicker {\n\tf.WithHeight(height)\n\treturn f\n}\n\n// Validate sets the validation function of the file field.\nfunc (f *FilePicker) Validate(validate func(string) error) *FilePicker {\n\tf.validate = validate\n\treturn f\n}\n\n// Error returns the error of the file field.\nfunc (f *FilePicker) Error() error {\n\treturn f.err\n}\n\n// Skip returns whether the file should be skipped or should be blocking.\nfunc (*FilePicker) Skip() bool {\n\treturn false\n}\n\n// Zoom returns whether the input should be zoomed.\nfunc (f *FilePicker) Zoom() bool {\n\treturn f.picking\n}\n\n// Focus focuses the file field.\nfunc (f *FilePicker) Focus() tea.Cmd {\n\tf.focused = true\n\treturn f.picker.Init()\n}\n\n// Blur blurs the file field.\nfunc (f *FilePicker) Blur() tea.Cmd {\n\tf.focused = false\n\tf.setPicking(false)\n\tf.err = f.validate(f.accessor.Get())\n\treturn nil\n}\n\n// KeyBinds returns the help keybindings for the file field.\nfunc (f *FilePicker) KeyBinds() []key.Binding {\n\treturn []key.Binding{f.keymap.Up, f.keymap.Down, f.keymap.Close, f.keymap.Open, f.keymap.Prev, f.keymap.Next, f.keymap.Submit}\n}\n\n// Init initializes the file field.\nfunc (f *FilePicker) Init() tea.Cmd {\n\treturn f.picker.Init()\n}\n\n// Update updates the file field.\nfunc (f *FilePicker) Update(msg tea.Msg) (Model, tea.Cmd) {\n\tf.err = nil\n\n\tswitch msg := msg.(type) {\n\tcase tea.BackgroundColorMsg:\n\t\tf.hasDarkBg = msg.IsDark()\n\tcase tea.KeyPressMsg:\n\t\tswitch {\n\t\tcase key.Matches(msg, f.keymap.Open):\n\t\t\tif f.picking {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tf.setPicking(true)\n\t\t\treturn f, f.picker.Init()\n\t\tcase key.Matches(msg, f.keymap.Close):\n\t\t\tf.setPicking(false)\n\t\t\treturn f, NextField\n\t\tcase key.Matches(msg, f.keymap.Next):\n\t\t\tf.setPicking(false)\n\t\t\treturn f, NextField\n\t\tcase key.Matches(msg, f.keymap.Prev):\n\t\t\tf.setPicking(false)\n\t\t\treturn f, PrevField\n\t\t}\n\t}\n\n\tvar cmd tea.Cmd\n\tf.picker, cmd = f.picker.Update(msg)\n\tdidSelect, file := f.picker.DidSelectFile(msg)\n\tif didSelect {\n\t\tf.accessor.Set(file)\n\t\tf.setPicking(false)\n\t\treturn f, NextField\n\t}\n\tdidSelect, _ = f.picker.DidSelectDisabledFile(msg)\n\tif didSelect {\n\t\tf.err = errors.New(xstrings.EnglishJoin(f.picker.AllowedTypes, true) + \" files only\")\n\t\treturn f, nil\n\t}\n\n\treturn f, cmd\n}\n\nfunc (f *FilePicker) activeStyles() *FieldStyles {\n\ttheme := f.theme\n\tif theme == nil {\n\t\ttheme = ThemeFunc(ThemeCharm)\n\t}\n\tif f.focused {\n\t\treturn &theme.Theme(f.hasDarkBg).Focused\n\t}\n\treturn &theme.Theme(f.hasDarkBg).Blurred\n}\n\nfunc (f *FilePicker) renderTitle() string {\n\tstyles := f.activeStyles()\n\tmaxWidth := f.width - styles.Base.GetHorizontalFrameSize()\n\treturn styles.Title.Render(wrap(f.title, maxWidth))\n}\n\nfunc (f FilePicker) renderDescription() string {\n\tstyles := f.activeStyles()\n\tmaxWidth := f.width - styles.Base.GetHorizontalFrameSize()\n\treturn styles.Description.Render(wrap(f.description, maxWidth))\n}\n\n// View renders the file field.\nfunc (f *FilePicker) View() string {\n\tstyles := f.activeStyles()\n\tvar parts []string\n\tif f.title != \"\" {\n\t\tparts = append(parts, f.renderTitle())\n\t}\n\tif f.description != \"\" {\n\t\tparts = append(parts, f.renderDescription())\n\t}\n\tparts = append(parts, f.pickerView())\n\treturn styles.Base.Width(f.width).Height(f.height).\n\t\tRender(strings.Join(parts, \"\\n\"))\n}\n\nfunc (f *FilePicker) pickerView() string {\n\tif f.picking {\n\t\treturn f.picker.View()\n\t}\n\tstyles := f.activeStyles()\n\tif f.accessor.Get() != \"\" {\n\t\treturn styles.SelectedOption.Render(f.accessor.Get())\n\t}\n\treturn styles.TextInput.Placeholder.Render(\"No file selected.\")\n}\n\nfunc (f *FilePicker) setPicking(v bool) {\n\tf.picking = v\n\n\tf.keymap.Close.SetEnabled(v)\n\tf.keymap.Up.SetEnabled(v)\n\tf.keymap.Down.SetEnabled(v)\n\tf.keymap.Select.SetEnabled(v)\n\tf.keymap.Back.SetEnabled(v)\n\n\tf.picker.KeyMap.Up.SetEnabled(v)\n\tf.picker.KeyMap.Down.SetEnabled(v)\n\tf.picker.KeyMap.GoToTop.SetEnabled(v)\n\tf.picker.KeyMap.GoToLast.SetEnabled(v)\n\tf.picker.KeyMap.Select.SetEnabled(v)\n\tf.picker.KeyMap.Open.SetEnabled(v)\n\tf.picker.KeyMap.Back.SetEnabled(v)\n}\n\n// Run runs the file field.\nfunc (f *FilePicker) Run() error {\n\treturn Run(f)\n}\n\n// RunAccessible runs an accessible file field.\nfunc (f *FilePicker) RunAccessible(w io.Writer, r io.Reader) error {\n\tstyles := f.activeStyles()\n\tprompt := styles.Title.\n\t\tPaddingRight(1).\n\t\tRender(cmp.Or(f.title, \"Choose a file:\"))\n\n\tvalidateFile := func(s string) error {\n\t\t// is the string a file?\n\t\tif _, err := os.Open(s); err != nil {\n\t\t\treturn errors.New(\"not a file\")\n\t\t}\n\n\t\t// is it one of the allowed types?\n\t\tvalid := len(f.picker.AllowedTypes) == 0\n\t\tfor _, ext := range f.picker.AllowedTypes {\n\t\t\tif strings.HasSuffix(s, ext) {\n\t\t\t\tvalid = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !valid {\n\t\t\treturn errors.New(\"cannot select: \" + s)\n\t\t}\n\n\t\t// does it pass user validation?\n\t\treturn f.validate(s)\n\t}\n\n\tf.accessor.Set(accessibility.PromptString(\n\t\tw,\n\t\tr,\n\t\tprompt,\n\t\tf.GetValue().(string),\n\t\tvalidateFile,\n\t))\n\treturn nil\n}\n\n// copied from bubbles' filepicker.\nconst (\n\tfileSizeWidth = 7\n\tpaddingLeft   = 2\n)\n\n// WithTheme sets the theme of the file field.\nfunc (f *FilePicker) WithTheme(theme Theme) Field {\n\tif f.theme != nil || theme == nil {\n\t\treturn f\n\t}\n\tf.theme = theme\n\tstyles := f.theme.Theme(f.hasDarkBg)\n\n\t// XXX: add specific themes\n\tf.picker.Styles = filepicker.Styles{\n\t\tDisabledCursor:   lipgloss.Style{},\n\t\tCursor:           styles.Focused.TextInput.Prompt,\n\t\tSymlink:          lipgloss.NewStyle(),\n\t\tDirectory:        styles.Focused.Directory,\n\t\tFile:             styles.Focused.File,\n\t\tDisabledFile:     styles.Focused.TextInput.Placeholder,\n\t\tPermission:       styles.Focused.TextInput.Placeholder,\n\t\tSelected:         styles.Focused.SelectedOption,\n\t\tDisabledSelected: styles.Focused.TextInput.Placeholder,\n\t\tFileSize:         styles.Focused.TextInput.Placeholder.Width(fileSizeWidth).Align(lipgloss.Right),\n\t\tEmptyDirectory:   styles.Focused.TextInput.Placeholder.PaddingLeft(paddingLeft).SetString(\"No files found.\"),\n\t}\n\n\treturn f\n}\n\n// WithKeyMap sets the keymap on a file field.\nfunc (f *FilePicker) WithKeyMap(k *KeyMap) Field {\n\tf.keymap = k.FilePicker\n\tf.picker.KeyMap = filepicker.KeyMap{\n\t\tGoToTop:  k.FilePicker.GotoTop,\n\t\tGoToLast: k.FilePicker.GotoBottom,\n\t\tDown:     k.FilePicker.Down,\n\t\tUp:       k.FilePicker.Up,\n\t\tPageUp:   k.FilePicker.PageUp,\n\t\tPageDown: k.FilePicker.PageDown,\n\t\tBack:     k.FilePicker.Back,\n\t\tOpen:     k.FilePicker.Open,\n\t\tSelect:   k.FilePicker.Select,\n\t}\n\tf.setPicking(f.picking)\n\treturn f\n}\n\n// WithWidth sets the width of the file field.\nfunc (f *FilePicker) WithWidth(width int) Field {\n\tf.width = width\n\treturn f\n}\n\n// WithHeight sets the height of the file field.\nfunc (f *FilePicker) WithHeight(height int) Field {\n\tif height == 0 {\n\t\treturn f\n\t}\n\tadjust := 0\n\tif f.title != \"\" {\n\t\tadjust += lipgloss.Height(f.renderTitle())\n\t}\n\tif f.description != \"\" {\n\t\tadjust += lipgloss.Height(f.renderDescription())\n\t}\n\tadjust++ // picker's own help height\n\tf.picker.SetHeight(height - adjust)\n\treturn f\n}\n\n// WithPosition sets the position of the file field.\nfunc (f *FilePicker) WithPosition(p FieldPosition) Field {\n\tf.keymap.Prev.SetEnabled(!p.IsFirst())\n\tf.keymap.Next.SetEnabled(!p.IsLast())\n\tf.keymap.Submit.SetEnabled(p.IsLast())\n\treturn f\n}\n\n// GetKey returns the key of the field.\nfunc (f *FilePicker) GetKey() string {\n\treturn f.key\n}\n\n// GetValue returns the value of the field.\nfunc (f *FilePicker) GetValue() any {\n\treturn f.accessor.Get()\n}\n"
  },
  {
    "path": "field_input.go",
    "content": "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/textinput\"\n\ttea \"charm.land/bubbletea/v2\"\n\t\"charm.land/huh/v2/internal/accessibility\"\n\t\"charm.land/lipgloss/v2\"\n)\n\n// Input is a input field.\n//\n// The input field is a field that allows the user to enter text. Use it to user\n// input. It can be used for collecting text, passwords, or other short input.\n//\n// The input field supports Suggestions, Placeholder, and Validation.\ntype Input struct {\n\taccessor Accessor[string]\n\tkey      string\n\tid       int\n\n\ttitle       Eval[string]\n\tdescription Eval[string]\n\tplaceholder Eval[string]\n\tsuggestions Eval[[]string]\n\n\ttextinput textinput.Model\n\n\tinline   bool\n\tvalidate func(string) error\n\terr      error\n\tfocused  bool\n\n\twidth  int\n\theight int\n\n\ttheme     Theme\n\thasDarkBg bool\n\tkeymap    InputKeyMap\n}\n\n// NewInput creates a new input field.\n//\n// The input field is a field that allows the user to enter text. Use it to user\n// input. It can be used for collecting text, passwords, or other short input.\n//\n// The input field supports Suggestions, Placeholder, and Validation.\nfunc NewInput() *Input {\n\tinput := textinput.New()\n\n\ti := &Input{\n\t\taccessor:    &EmbeddedAccessor[string]{},\n\t\ttextinput:   input,\n\t\tvalidate:    func(string) error { return nil },\n\t\tid:          nextID(),\n\t\ttitle:       Eval[string]{cache: make(map[uint64]string)},\n\t\tdescription: Eval[string]{cache: make(map[uint64]string)},\n\t\tplaceholder: Eval[string]{cache: make(map[uint64]string)},\n\t\tsuggestions: Eval[[]string]{cache: make(map[uint64][]string)},\n\t}\n\n\treturn i\n}\n\n// Value sets the value of the input field.\nfunc (i *Input) Value(value *string) *Input {\n\treturn i.Accessor(NewPointerAccessor(value))\n}\n\n// Accessor sets the accessor of the input field.\nfunc (i *Input) Accessor(accessor Accessor[string]) *Input {\n\ti.accessor = accessor\n\ti.textinput.SetValue(i.accessor.Get())\n\treturn i\n}\n\n// Key sets the key of the input field.\nfunc (i *Input) Key(key string) *Input {\n\ti.key = key\n\treturn i\n}\n\n// Title sets the title of the input field.\n//\n// The Title is static for dynamic Title use `TitleFunc`.\nfunc (i *Input) Title(title string) *Input {\n\ti.title.val = title\n\ti.title.fn = nil\n\treturn i\n}\n\n// Description sets the description of the input field.\n//\n// The Description is static for dynamic Description use `DescriptionFunc`.\nfunc (i *Input) Description(description string) *Input {\n\ti.description.val = description\n\ti.description.fn = nil\n\treturn i\n}\n\n// TitleFunc sets the title func of the input field.\n//\n// The TitleFunc will be re-evaluated when the binding of the TitleFunc changes.\n// This is useful when you want to display dynamic content and update the title\n// when another part of your form changes.\n//\n// See README#Dynamic for more usage information.\nfunc (i *Input) TitleFunc(f func() string, bindings any) *Input {\n\ti.title.fn = f\n\ti.title.bindings = bindings\n\treturn i\n}\n\n// DescriptionFunc sets the description func of the input field.\n//\n// The DescriptionFunc will be re-evaluated when the binding of the\n// DescriptionFunc changes. This is useful when you want to display dynamic\n// content and update the description when another part of your form changes.\n//\n// See README#Dynamic for more usage information.\nfunc (i *Input) DescriptionFunc(f func() string, bindings any) *Input {\n\ti.description.fn = f\n\ti.description.bindings = bindings\n\treturn i\n}\n\n// Prompt sets the prompt of the input field.\nfunc (i *Input) Prompt(prompt string) *Input {\n\ti.textinput.Prompt = prompt\n\treturn i\n}\n\n// CharLimit sets the character limit of the input field.\nfunc (i *Input) CharLimit(charlimit int) *Input {\n\ti.textinput.CharLimit = charlimit\n\treturn i\n}\n\n// Suggestions sets the suggestions to display for autocomplete in the input\n// field.\n//\n// The suggestions are static for dynamic suggestions use `SuggestionsFunc`.\nfunc (i *Input) Suggestions(suggestions []string) *Input {\n\ti.suggestions.fn = nil\n\n\ti.textinput.ShowSuggestions = len(suggestions) > 0\n\ti.textinput.KeyMap.AcceptSuggestion.SetEnabled(len(suggestions) > 0)\n\ti.textinput.SetSuggestions(suggestions)\n\treturn i\n}\n\n// SuggestionsFunc sets the suggestions func to display for autocomplete in the\n// input field.\n//\n// The SuggestionsFunc will be re-evaluated when the binding of the\n// SuggestionsFunc changes. This is useful when you want to display dynamic\n// suggestions when another part of your form changes.\n//\n// See README#Dynamic for more usage information.\nfunc (i *Input) SuggestionsFunc(f func() []string, bindings any) *Input {\n\ti.suggestions.fn = f\n\ti.suggestions.bindings = bindings\n\ti.suggestions.loading = true\n\n\ti.textinput.KeyMap.AcceptSuggestion.SetEnabled(f != nil)\n\ti.textinput.ShowSuggestions = f != nil\n\treturn i\n}\n\n// EchoMode sets the input behavior of the text Input field.\ntype EchoMode textinput.EchoMode\n\nconst (\n\t// EchoModeNormal displays text as is.\n\t// This is the default behavior.\n\tEchoModeNormal EchoMode = EchoMode(textinput.EchoNormal)\n\n\t// EchoModePassword displays the EchoCharacter mask instead of actual characters.\n\t// This is commonly used for password fields.\n\tEchoModePassword EchoMode = EchoMode(textinput.EchoPassword)\n\n\t// EchoModeNone displays nothing as characters are entered.\n\t// This is commonly seen for password fields on the command line.\n\tEchoModeNone EchoMode = EchoMode(textinput.EchoNone)\n)\n\n// EchoMode sets the echo mode of the input.\nfunc (i *Input) EchoMode(mode EchoMode) *Input {\n\ti.textinput.EchoMode = textinput.EchoMode(mode)\n\treturn i\n}\n\n// Password sets whether or not to hide the input while the user is typing.\n//\n// Deprecated: use EchoMode(EchoPassword) instead.\nfunc (i *Input) Password(password bool) *Input {\n\tif password {\n\t\ti.textinput.EchoMode = textinput.EchoPassword\n\t} else {\n\t\ti.textinput.EchoMode = textinput.EchoNormal\n\t}\n\treturn i\n}\n\n// Placeholder sets the placeholder of the text input.\nfunc (i *Input) Placeholder(str string) *Input {\n\ti.textinput.Placeholder = str\n\treturn i\n}\n\n// PlaceholderFunc sets the placeholder func of the text input.\nfunc (i *Input) PlaceholderFunc(f func() string, bindings any) *Input {\n\ti.placeholder.fn = f\n\ti.placeholder.bindings = bindings\n\treturn i\n}\n\n// Inline sets whether the title and input should be on the same line.\nfunc (i *Input) Inline(inline bool) *Input {\n\ti.inline = inline\n\treturn i\n}\n\n// Validate sets the validation function of the input field.\nfunc (i *Input) Validate(validate func(string) error) *Input {\n\ti.validate = validate\n\treturn i\n}\n\n// Error returns the error of the input field.\nfunc (i *Input) Error() error { return i.err }\n\n// Skip returns whether the input should be skipped or should be blocking.\nfunc (*Input) Skip() bool { return false }\n\n// Zoom returns whether the input should be zoomed.\nfunc (*Input) Zoom() bool { return false }\n\n// Focus focuses the input field.\nfunc (i *Input) Focus() tea.Cmd {\n\ti.focused = true\n\treturn i.textinput.Focus()\n}\n\n// Blur blurs the input field.\nfunc (i *Input) Blur() tea.Cmd {\n\ti.focused = false\n\ti.accessor.Set(i.textinput.Value())\n\ti.textinput.Blur()\n\ti.err = i.validate(i.accessor.Get())\n\treturn nil\n}\n\n// KeyBinds returns the help message for the input field.\nfunc (i *Input) KeyBinds() []key.Binding {\n\tif i.textinput.ShowSuggestions {\n\t\treturn []key.Binding{i.keymap.AcceptSuggestion, i.keymap.Prev, i.keymap.Submit, i.keymap.Next}\n\t}\n\treturn []key.Binding{i.keymap.Prev, i.keymap.Submit, i.keymap.Next}\n}\n\n// Init initializes the input field.\nfunc (i *Input) Init() tea.Cmd {\n\ti.textinput.Blur()\n\treturn nil\n}\n\n// Update updates the input field.\nfunc (i *Input) Update(msg tea.Msg) (Model, tea.Cmd) {\n\tvar cmds []tea.Cmd //nolint:prealloc\n\n\tswitch msg := msg.(type) {\n\tcase tea.BackgroundColorMsg:\n\t\ti.hasDarkBg = msg.IsDark()\n\tcase updateFieldMsg:\n\t\tvar cmds []tea.Cmd\n\t\tif ok, hash := i.title.shouldUpdate(); ok {\n\t\t\ti.title.bindingsHash = hash\n\t\t\tif !i.title.loadFromCache() {\n\t\t\t\ti.title.loading = true\n\t\t\t\tcmds = append(cmds, func() tea.Msg {\n\t\t\t\t\treturn updateTitleMsg{id: i.id, title: i.title.fn(), hash: hash}\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t\tif ok, hash := i.description.shouldUpdate(); ok {\n\t\t\ti.description.bindingsHash = hash\n\t\t\tif !i.description.loadFromCache() {\n\t\t\t\ti.description.loading = true\n\t\t\t\tcmds = append(cmds, func() tea.Msg {\n\t\t\t\t\treturn updateDescriptionMsg{id: i.id, description: i.description.fn(), hash: hash}\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t\tif ok, hash := i.placeholder.shouldUpdate(); ok {\n\t\t\ti.placeholder.bindingsHash = hash\n\t\t\tif i.placeholder.loadFromCache() {\n\t\t\t\ti.textinput.Placeholder = i.placeholder.val\n\t\t\t} else {\n\t\t\t\ti.placeholder.loading = true\n\t\t\t\tcmds = append(cmds, func() tea.Msg {\n\t\t\t\t\treturn updatePlaceholderMsg{id: i.id, placeholder: i.placeholder.fn(), hash: hash}\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t\tif ok, hash := i.suggestions.shouldUpdate(); ok {\n\t\t\ti.suggestions.bindingsHash = hash\n\t\t\tif i.suggestions.loadFromCache() {\n\t\t\t\ti.textinput.ShowSuggestions = len(i.suggestions.val) > 0\n\t\t\t\ti.textinput.SetSuggestions(i.suggestions.val)\n\t\t\t} else {\n\t\t\t\ti.suggestions.loading = true\n\t\t\t\tcmds = append(cmds, func() tea.Msg {\n\t\t\t\t\treturn updateSuggestionsMsg{id: i.id, suggestions: i.suggestions.fn(), hash: hash}\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t\treturn i, tea.Batch(cmds...)\n\tcase updateTitleMsg:\n\t\tif i.id == msg.id && i.title.bindingsHash == msg.hash {\n\t\t\ti.title.update(msg.title)\n\t\t}\n\tcase updateDescriptionMsg:\n\t\tif i.id == msg.id && i.description.bindingsHash == msg.hash {\n\t\t\ti.description.update(msg.description)\n\t\t}\n\tcase updatePlaceholderMsg:\n\t\tif i.id == msg.id && i.placeholder.bindingsHash == msg.hash {\n\t\t\ti.placeholder.update(msg.placeholder)\n\t\t\ti.textinput.Placeholder = msg.placeholder\n\t\t}\n\tcase updateSuggestionsMsg:\n\t\tif i.id == msg.id && i.suggestions.bindingsHash == msg.hash {\n\t\t\ti.suggestions.update(msg.suggestions)\n\t\t\ti.textinput.ShowSuggestions = len(msg.suggestions) > 0\n\t\t\ti.textinput.SetSuggestions(msg.suggestions)\n\t\t}\n\tcase tea.KeyPressMsg:\n\t\ti.err = nil\n\n\t\tswitch {\n\t\tcase key.Matches(msg, i.keymap.Prev):\n\t\t\tcmds = append(cmds, PrevField)\n\t\tcase key.Matches(msg, i.keymap.Next, i.keymap.Submit):\n\t\t\tvalue := i.textinput.Value()\n\t\t\ti.err = i.validate(value)\n\t\t\tif i.err != nil {\n\t\t\t\treturn i, nil\n\t\t\t}\n\t\t\tcmds = append(cmds, NextField)\n\t\t}\n\t}\n\n\tvar cmd tea.Cmd\n\ti.textinput, cmd = i.textinput.Update(msg)\n\tcmds = append(cmds, cmd)\n\ti.accessor.Set(i.textinput.Value())\n\n\treturn i, tea.Batch(cmds...)\n}\n\nfunc (i *Input) activeStyles() *FieldStyles {\n\ttheme := i.theme\n\tif theme == nil {\n\t\ttheme = ThemeFunc(ThemeCharm)\n\t}\n\tif i.focused {\n\t\treturn &theme.Theme(i.hasDarkBg).Focused\n\t}\n\treturn &theme.Theme(i.hasDarkBg).Blurred\n}\n\n// View renders the input field.\nfunc (i *Input) View() string {\n\tstyles := i.activeStyles()\n\tmaxWidth := i.width - styles.Base.GetHorizontalFrameSize()\n\n\t// NB: since the method is on a pointer receiver these are being mutated.\n\t// Because this runs on every render this shouldn't matter in practice,\n\t// however.\n\tst := i.textinput.Styles()\n\tst.Cursor.Color = styles.TextInput.Cursor.GetForeground()\n\tst.Focused.Prompt = styles.TextInput.Prompt\n\tst.Focused.Text = styles.TextInput.Text\n\tst.Focused.Placeholder = styles.TextInput.Placeholder\n\ti.textinput.SetStyles(st)\n\n\t// Adjust text input size to its char limit if it fit in its width\n\tif i.textinput.CharLimit > 0 {\n\t\ti.textinput.SetWidth(max(min(i.textinput.CharLimit, i.textinput.Width(), maxWidth), 0))\n\t}\n\n\tvar sb strings.Builder\n\tif i.title.val != \"\" || i.title.fn != nil {\n\t\tsb.WriteString(styles.Title.Render(wrap(i.title.val, maxWidth)))\n\t\tif !i.inline {\n\t\t\tsb.WriteString(\"\\n\")\n\t\t}\n\t}\n\tif i.description.val != \"\" || i.description.fn != nil {\n\t\tsb.WriteString(styles.Description.Render(wrap(i.description.val, maxWidth)))\n\t\tif !i.inline {\n\t\t\tsb.WriteString(\"\\n\")\n\t\t}\n\t}\n\tsb.WriteString(i.textinput.View())\n\n\treturn styles.Base.\n\t\tWidth(i.width).\n\t\tHeight(i.height).\n\t\tRender(sb.String())\n}\n\n// Run runs the input field in accessible mode.\nfunc (i *Input) Run() error {\n\treturn i.run()\n}\n\n// run runs the input field.\nfunc (i *Input) run() error {\n\treturn Run(i)\n}\n\n// RunAccessible runs the input field in accessible mode.\nfunc (i *Input) RunAccessible(w io.Writer, r io.Reader) error {\n\tstyles := i.activeStyles()\n\tvalidator := func(input string) error {\n\t\tif i.textinput.CharLimit > 0 && len(input) > i.textinput.CharLimit {\n\t\t\treturn fmt.Errorf(\"Input cannot exceed %d characters\", i.textinput.CharLimit)\n\t\t}\n\t\treturn i.validate(input)\n\t}\n\n\tswitch i.textinput.EchoMode {\n\tcase textinput.EchoNormal:\n\t\tprompt := styles.Title.\n\t\t\tPaddingRight(1).\n\t\t\tRender(cmp.Or(i.title.val, \"Input:\"))\n\t\tvalue := accessibility.PromptString(w, r, prompt, i.GetValue().(string), validator)\n\t\ti.accessor.Set(value)\n\t\treturn nil\n\tdefault:\n\t\tprompt := styles.Title.\n\t\t\tPaddingRight(1).\n\t\t\tRender(cmp.Or(i.title.val, \"Password:\"))\n\t\tif fd, ok := r.(interface{ Fd() uintptr }); ok {\n\t\t\tvalue, err := accessibility.PromptPassword(w, fd.Fd(), prompt, validator)\n\t\t\tif err != nil {\n\t\t\t\treturn err //nolint:wrapcheck\n\t\t\t}\n\t\t\ti.accessor.Set(value)\n\t\t\treturn nil\n\t\t}\n\t\treturn errors.New(\"password asking needs a tty\")\n\t}\n}\n\n// WithKeyMap sets the keymap on an input field.\nfunc (i *Input) WithKeyMap(k *KeyMap) Field {\n\ti.keymap = k.Input\n\ti.textinput.KeyMap.AcceptSuggestion = i.keymap.AcceptSuggestion\n\treturn i\n}\n\n// WithTheme sets the theme of the input field.\nfunc (i *Input) WithTheme(theme Theme) Field {\n\tif i.theme != nil {\n\t\treturn i\n\t}\n\ti.theme = theme\n\treturn i\n}\n\n// WithWidth sets the width of the input field.\nfunc (i *Input) WithWidth(width int) Field {\n\tstyles := i.activeStyles()\n\ti.width = width\n\tframeSize := styles.Base.GetHorizontalFrameSize()\n\tpromptWidth := lipgloss.Width(i.textinput.Styles().Focused.Prompt.Render(i.textinput.Prompt))\n\ttitleWidth := lipgloss.Width(styles.Title.Render(i.title.val))\n\tdescriptionWidth := lipgloss.Width(styles.Description.Render(i.description.val))\n\ti.textinput.SetWidth(width - frameSize - promptWidth - 1)\n\tif i.inline {\n\t\ti.textinput.SetWidth(i.textinput.Width() - titleWidth - descriptionWidth)\n\t}\n\treturn i\n}\n\n// WithHeight sets the height of the input field.\nfunc (i *Input) WithHeight(height int) Field {\n\ti.height = height\n\treturn i\n}\n\n// WithPosition sets the position of the input field.\nfunc (i *Input) WithPosition(p FieldPosition) Field {\n\ti.keymap.Prev.SetEnabled(!p.IsFirst())\n\ti.keymap.Next.SetEnabled(!p.IsLast())\n\ti.keymap.Submit.SetEnabled(p.IsLast())\n\treturn i\n}\n\n// GetKey returns the key of the field.\nfunc (i *Input) GetKey() string { return i.key }\n\n// GetValue returns the value of the field.\nfunc (i *Input) GetValue() any {\n\treturn i.accessor.Get()\n}\n"
  },
  {
    "path": "field_multiselect.go",
    "content": "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/bubbles/v2/spinner\"\n\t\"charm.land/bubbles/v2/textinput\"\n\t\"charm.land/bubbles/v2/viewport\"\n\ttea \"charm.land/bubbletea/v2\"\n\t\"charm.land/huh/v2/internal/accessibility\"\n\t\"charm.land/lipgloss/v2\"\n\t\"github.com/charmbracelet/x/exp/ordered\"\n)\n\n// MultiSelect is a form multi-select field.\ntype MultiSelect[T comparable] struct {\n\taccessor Accessor[[]T]\n\tkey      string\n\tid       int\n\n\t// customization\n\ttitle           Eval[string]\n\tdescription     Eval[string]\n\toptions         Eval[[]Option[T]]\n\tfilterable      bool\n\tfilteredOptions []Option[T]\n\tlimit           int\n\n\t// error handling\n\tvalidate func([]T) error\n\terr      error\n\n\t// state\n\tcursor    int\n\tfocused   bool\n\tfiltering bool\n\tfilter    textinput.Model\n\tviewport  viewport.Model\n\tspinner   spinner.Model\n\n\t// options\n\twidth     int\n\theight    int\n\ttheme     Theme\n\thasDarkBg bool\n\tkeymap    MultiSelectKeyMap\n}\n\n// NewMultiSelect returns a new multi-select field.\nfunc NewMultiSelect[T comparable]() *MultiSelect[T] {\n\tfilter := textinput.New()\n\tfilter.Prompt = \"/\"\n\n\ts := spinner.New(spinner.WithSpinner(spinner.Line))\n\n\treturn &MultiSelect[T]{\n\t\taccessor:    &EmbeddedAccessor[[]T]{},\n\t\tvalidate:    func([]T) error { return nil },\n\t\tfiltering:   false,\n\t\tfilter:      filter,\n\t\tid:          nextID(),\n\t\toptions:     Eval[[]Option[T]]{cache: make(map[uint64][]Option[T])},\n\t\ttitle:       Eval[string]{cache: make(map[uint64]string)},\n\t\tdescription: Eval[string]{cache: make(map[uint64]string)},\n\t\tspinner:     s,\n\t\tfilterable:  true,\n\t}\n}\n\n// Value sets the value of the multi-select field.\nfunc (m *MultiSelect[T]) Value(value *[]T) *MultiSelect[T] {\n\treturn m.Accessor(NewPointerAccessor(value))\n}\n\n// Accessor sets the accessor of the input field.\nfunc (m *MultiSelect[T]) Accessor(accessor Accessor[[]T]) *MultiSelect[T] {\n\tm.accessor = accessor\n\tfor i, o := range m.options.val {\n\t\tif slices.Contains(m.accessor.Get(), o.Value) {\n\t\t\tm.options.val[i].selected = true\n\t\t}\n\t}\n\treturn m\n}\n\n// Key sets the key of the select field which can be used to retrieve the value\n// after submission.\nfunc (m *MultiSelect[T]) Key(key string) *MultiSelect[T] {\n\tm.key = key\n\treturn m\n}\n\n// Title sets the title of the multi-select field.\nfunc (m *MultiSelect[T]) Title(title string) *MultiSelect[T] {\n\tm.title.val = title\n\tm.title.fn = nil\n\treturn m\n}\n\n// TitleFunc sets the title func of the multi-select field.\nfunc (m *MultiSelect[T]) TitleFunc(f func() string, bindings any) *MultiSelect[T] {\n\tm.title.fn = f\n\tm.title.bindings = bindings\n\treturn m\n}\n\n// Description sets the description of the multi-select field.\nfunc (m *MultiSelect[T]) Description(description string) *MultiSelect[T] {\n\tm.description.val = description\n\treturn m\n}\n\n// DescriptionFunc sets the description func of the multi-select field.\nfunc (m *MultiSelect[T]) DescriptionFunc(f func() string, bindings any) *MultiSelect[T] {\n\tm.description.fn = f\n\tm.description.bindings = bindings\n\treturn m\n}\n\n// Options sets the options of the multi-select field.\nfunc (m *MultiSelect[T]) Options(options ...Option[T]) *MultiSelect[T] {\n\tif len(options) <= 0 {\n\t\treturn m\n\t}\n\n\tm.options.val = options\n\tm.filteredOptions = options\n\tm.selectOptions()\n\tm.updateViewportSize()\n\treturn m\n}\n\nfunc (m *MultiSelect[T]) selectOptions() {\n\t// Set the cursor to the existing value or the last selected option.\n\tfor i, o := range m.options.val {\n\t\tfor _, v := range m.accessor.Get() {\n\t\t\tif o.Value == v {\n\t\t\t\tm.options.val[i].selected = true\n\t\t\t}\n\t\t}\n\t}\n\n\tfor i, o := range m.options.val {\n\t\tif !o.selected {\n\t\t\tcontinue\n\t\t}\n\t\tm.cursor = i\n\t\tm.ensureCursorVisible()\n\t\tbreak\n\t}\n}\n\n// OptionsFunc sets the options func of the multi-select field.\nfunc (m *MultiSelect[T]) OptionsFunc(f func() []Option[T], bindings any) *MultiSelect[T] {\n\tm.options.fn = f\n\tm.options.bindings = bindings\n\tm.filteredOptions = make([]Option[T], 0)\n\t// If there is no height set, we should attach a static height since these\n\t// options are possibly dynamic.\n\tif m.height <= 0 {\n\t\tm.height = defaultHeight\n\t\tm.updateViewportSize()\n\t}\n\tif m.width <= 0 {\n\t\tm.Width(20)\n\t}\n\treturn m\n}\n\n// Filterable sets the multi-select field as filterable.\nfunc (m *MultiSelect[T]) Filterable(filterable bool) *MultiSelect[T] {\n\tm.filterable = filterable\n\treturn m\n}\n\n// Filtering sets the filtering state of the multi-select field.\nfunc (m *MultiSelect[T]) Filtering(filtering bool) *MultiSelect[T] {\n\tm.filtering = filtering\n\tm.filter.Focus()\n\treturn m\n}\n\n// Limit sets the limit of the multi-select field.\nfunc (m *MultiSelect[T]) Limit(limit int) *MultiSelect[T] {\n\tm.limit = limit\n\tm.setSelectAllHelp()\n\treturn m\n}\n\n// Width sets the width of the multi-select field.\nfunc (m *MultiSelect[T]) Width(width int) *MultiSelect[T] {\n\t// What we really want to do is set the width of the viewport, but we\n\t// need a theme applied before we can calcualate its width.\n\tm.width = width\n\tm.updateViewportSize()\n\treturn m\n}\n\n// Height sets the height of the multi-select field.\nfunc (m *MultiSelect[T]) Height(height int) *MultiSelect[T] {\n\t// What we really want to do is set the height of the viewport, but we\n\t// need a theme applied before we can calcualate its height.\n\tm.height = height\n\tm.updateViewportSize()\n\treturn m\n}\n\n// Validate sets the validation function of the multi-select field.\nfunc (m *MultiSelect[T]) Validate(validate func([]T) error) *MultiSelect[T] {\n\tm.validate = validate\n\treturn m\n}\n\n// Error returns the error of the multi-select field.\nfunc (m *MultiSelect[T]) Error() error {\n\treturn m.err\n}\n\n// Skip returns whether the multiselect should be skipped or should be blocking.\nfunc (*MultiSelect[T]) Skip() bool {\n\treturn false\n}\n\n// Zoom returns whether the multiselect should be zoomed.\nfunc (*MultiSelect[T]) Zoom() bool {\n\treturn false\n}\n\n// Focus focuses the multi-select field.\nfunc (m *MultiSelect[T]) Focus() tea.Cmd {\n\tm.updateValue()\n\tm.focused = true\n\treturn nil\n}\n\n// Blur blurs the multi-select field.\nfunc (m *MultiSelect[T]) Blur() tea.Cmd {\n\tm.updateValue()\n\tm.focused = false\n\treturn nil\n}\n\n// Hovered returns the value of the option under the cursor, and a bool\n// indicating whether one was found. If there are no visible options, returns\n// a zero-valued T and false.\nfunc (m *MultiSelect[T]) Hovered() (T, bool) {\n\tif len(m.filteredOptions) == 0 || m.cursor >= len(m.filteredOptions) {\n\t\tvar zero T\n\t\treturn zero, false\n\t}\n\treturn m.filteredOptions[m.cursor].Value, true\n}\n\n// KeyBinds returns the help message for the multi-select field.\nfunc (m *MultiSelect[T]) KeyBinds() []key.Binding {\n\tm.setSelectAllHelp()\n\tbinds := []key.Binding{\n\t\tm.keymap.Toggle,\n\t\tm.keymap.Up,\n\t\tm.keymap.Down,\n\t}\n\tif m.filterable {\n\t\tbinds = append(\n\t\t\tbinds,\n\t\t\tm.keymap.Filter,\n\t\t\tm.keymap.SetFilter,\n\t\t\tm.keymap.ClearFilter,\n\t\t)\n\t}\n\tbinds = append(\n\t\tbinds,\n\t\tm.keymap.Prev,\n\t\tm.keymap.Submit,\n\t\tm.keymap.Next,\n\t\tm.keymap.SelectAll,\n\t\tm.keymap.SelectNone,\n\t)\n\treturn binds\n}\n\n// Init initializes the multi-select field.\nfunc (m *MultiSelect[T]) Init() tea.Cmd {\n\treturn nil\n}\n\n// Update updates the multi-select field.\nfunc (m *MultiSelect[T]) Update(msg tea.Msg) (Model, tea.Cmd) {\n\tvar cmds []tea.Cmd\n\n\t// Enforce height on the viewport during update as we need themes to\n\t// be applied before we can calculate the height.\n\tm.updateViewportSize()\n\n\tvar cmd tea.Cmd\n\tif m.filtering {\n\t\tm.filter, cmd = m.filter.Update(msg)\n\t\tm.setSelectAllHelp()\n\t\tcmds = append(cmds, cmd)\n\t}\n\n\tswitch msg := msg.(type) {\n\tcase tea.BackgroundColorMsg:\n\t\tm.hasDarkBg = msg.IsDark()\n\tcase updateFieldMsg:\n\t\tvar fieldCmds []tea.Cmd\n\t\tif ok, hash := m.title.shouldUpdate(); ok {\n\t\t\tm.title.bindingsHash = hash\n\t\t\tif !m.title.loadFromCache() {\n\t\t\t\tm.title.loading = true\n\t\t\t\tfieldCmds = append(fieldCmds, func() tea.Msg {\n\t\t\t\t\treturn updateTitleMsg{id: m.id, title: m.title.fn(), hash: hash}\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t\tif ok, hash := m.description.shouldUpdate(); ok {\n\t\t\tm.description.bindingsHash = hash\n\t\t\tif !m.description.loadFromCache() {\n\t\t\t\tm.description.loading = true\n\t\t\t\tfieldCmds = append(fieldCmds, func() tea.Msg {\n\t\t\t\t\treturn updateDescriptionMsg{id: m.id, description: m.description.fn(), hash: hash}\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t\tif ok, hash := m.options.shouldUpdate(); ok {\n\t\t\tm.options.bindingsHash = hash\n\t\t\tif m.options.loadFromCache() {\n\t\t\t\tm.filteredOptions = m.options.val\n\t\t\t\tm.updateValue()\n\t\t\t\tm.cursor = ordered.Clamp(m.cursor, 0, len(m.filteredOptions)-1)\n\t\t\t} else {\n\t\t\t\tm.options.loading = true\n\t\t\t\tm.options.loadingStart = time.Now()\n\t\t\t\tfieldCmds = append(fieldCmds, func() tea.Msg {\n\t\t\t\t\treturn updateOptionsMsg[T]{id: m.id, options: m.options.fn(), hash: hash}\n\t\t\t\t}, m.spinner.Tick)\n\t\t\t}\n\t\t}\n\n\t\treturn m, tea.Batch(fieldCmds...)\n\n\tcase spinner.TickMsg:\n\t\tif !m.options.loading {\n\t\t\tbreak\n\t\t}\n\t\tm.spinner, cmd = m.spinner.Update(msg)\n\t\treturn m, cmd\n\n\tcase updateTitleMsg:\n\t\tif msg.id == m.id && msg.hash == m.title.bindingsHash {\n\t\t\tm.title.update(msg.title)\n\t\t}\n\tcase updateDescriptionMsg:\n\t\tif msg.id == m.id && msg.hash == m.description.bindingsHash {\n\t\t\tm.description.update(msg.description)\n\t\t}\n\tcase updateOptionsMsg[T]:\n\t\tif msg.id == m.id && msg.hash == m.options.bindingsHash {\n\t\t\tm.options.update(msg.options)\n\t\t\tm.selectOptions()\n\t\t\t// since we're updating the options, we need to reset the cursor.\n\t\t\tm.filteredOptions = m.options.val\n\t\t\tm.updateValue()\n\t\t\tm.cursor = ordered.Clamp(m.cursor, 0, len(m.filteredOptions)-1)\n\t\t}\n\tcase tea.KeyPressMsg:\n\t\tm.err = nil\n\t\tswitch {\n\t\tcase key.Matches(msg, m.keymap.Filter):\n\t\t\tm.setFilter(true)\n\t\t\treturn m, m.filter.Focus()\n\t\tcase key.Matches(msg, m.keymap.SetFilter):\n\t\t\tif len(m.filteredOptions) <= 0 {\n\t\t\t\tm.filter.SetValue(\"\")\n\t\t\t\tm.filteredOptions = m.options.val\n\t\t\t}\n\t\t\tm.setFilter(false)\n\t\tcase key.Matches(msg, m.keymap.ClearFilter):\n\t\t\tm.filter.SetValue(\"\")\n\t\t\tm.filteredOptions = m.options.val\n\t\t\tm.setFilter(false)\n\t\tcase key.Matches(msg, m.keymap.Up):\n\t\t\t//nolint:godox\n\t\t\t// FIXME: should use keys in keymap\n\t\t\tif m.filtering && msg.String() == \"k\" {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tm.cursor = max(m.cursor-1, 0)\n\t\t\tm.ensureCursorVisible()\n\t\tcase key.Matches(msg, m.keymap.Down):\n\t\t\t//nolint:godox\n\t\t\t// FIXME: should use keys in keymap\n\t\t\tif m.filtering && msg.String() == \"j\" {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tm.cursor = min(m.cursor+1, len(m.filteredOptions)-1)\n\t\t\tm.ensureCursorVisible()\n\t\tcase key.Matches(msg, m.keymap.GotoTop):\n\t\t\tif m.filtering {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tm.cursor = 0\n\t\t\tm.viewport.GotoTop()\n\t\tcase key.Matches(msg, m.keymap.GotoBottom):\n\t\t\tif m.filtering {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tm.cursor = len(m.filteredOptions) - 1\n\t\t\tm.viewport.GotoBottom()\n\t\tcase key.Matches(msg, m.keymap.HalfPageUp):\n\t\t\tm.cursor = max(m.cursor-m.viewport.Height()/2, 0)\n\t\t\tm.ensureCursorVisible()\n\t\tcase key.Matches(msg, m.keymap.HalfPageDown):\n\t\t\tm.cursor = min(m.cursor+m.viewport.Height()/2, len(m.filteredOptions)-1)\n\t\t\tm.ensureCursorVisible()\n\t\tcase key.Matches(msg, m.keymap.Toggle) && !m.filtering:\n\t\t\tfor i, option := range m.options.val {\n\t\t\t\tif option.Key == m.filteredOptions[m.cursor].Key {\n\t\t\t\t\tif !m.options.val[m.cursor].selected && m.limit > 0 && m.numSelected() >= m.limit {\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t\tselected := m.options.val[i].selected\n\t\t\t\t\tm.options.val[i].selected = !selected\n\t\t\t\t\tm.filteredOptions[m.cursor].selected = !selected\n\t\t\t\t}\n\t\t\t}\n\t\t\tm.setSelectAllHelp()\n\t\t\tm.updateValue()\n\t\tcase key.Matches(msg, m.keymap.SelectAll, m.keymap.SelectNone) && m.limit <= 0:\n\t\t\tselected := false\n\n\t\t\tfor _, option := range m.filteredOptions {\n\t\t\t\tif !option.selected {\n\t\t\t\t\tselected = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfor i, option := range m.options.val {\n\t\t\t\tfor j := range m.filteredOptions {\n\t\t\t\t\tif option.Key == m.filteredOptions[j].Key {\n\t\t\t\t\t\tm.options.val[i].selected = selected\n\t\t\t\t\t\tm.filteredOptions[j].selected = selected\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tm.setSelectAllHelp()\n\t\t\tm.updateValue()\n\t\tcase key.Matches(msg, m.keymap.Prev):\n\t\t\tm.updateValue()\n\t\t\tm.err = m.validate(m.accessor.Get())\n\t\t\tif m.err != nil {\n\t\t\t\treturn m, nil\n\t\t\t}\n\t\t\treturn m, PrevField\n\t\tcase key.Matches(msg, m.keymap.Next, m.keymap.Submit):\n\t\t\tm.updateValue()\n\t\t\tm.err = m.validate(m.accessor.Get())\n\t\t\tif m.err != nil {\n\t\t\t\treturn m, nil\n\t\t\t}\n\t\t\treturn m, NextField\n\t\t}\n\n\t\tif m.filtering {\n\t\t\tm.filteredOptions = m.options.val\n\t\t\tif m.filter.Value() != \"\" {\n\t\t\t\tm.filteredOptions = nil\n\t\t\t\tfor _, option := range m.options.val {\n\t\t\t\t\tif m.filterFunc(option.Key) {\n\t\t\t\t\t\tm.filteredOptions = append(m.filteredOptions, option)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif len(m.filteredOptions) > 0 {\n\t\t\t\tm.cursor = min(m.cursor, len(m.filteredOptions)-1)\n\t\t\t}\n\t\t}\n\t\tm.ensureCursorVisible()\n\t}\n\n\treturn m, tea.Batch(cmds...)\n}\n\n// updateViewportSize updates the viewport size according to the Height setting\n// on this multi-select field.\nfunc (m *MultiSelect[T]) updateViewportSize() {\n\tyoffset := 0\n\tif ss := m.titleView(); ss != \"\" {\n\t\tyoffset += lipgloss.Height(ss)\n\t}\n\tif ss := m.descriptionView(); ss != \"\" {\n\t\tyoffset += lipgloss.Height(ss)\n\t}\n\tv, _, _ := m.optionsView()\n\theight := m.height\n\tif height <= 0 {\n\t\theight = lipgloss.Height(v)\n\t}\n\twidth := m.width\n\tif m.width <= 0 {\n\t\twidth = lipgloss.Width(v)\n\t}\n\n\tm.viewport.SetWidth(width)\n\tm.viewport.SetHeight(max(minHeight, height) - yoffset)\n}\n\n// numSelected returns the total number of selected options.\nfunc (m *MultiSelect[T]) numSelected() int {\n\tvar count int\n\tfor _, o := range m.options.val {\n\t\tif o.selected {\n\t\t\tcount++\n\t\t}\n\t}\n\treturn count\n}\n\n// numFilteredOptionsSelected returns the number of selected options with the\n// current filter applied.\nfunc (m *MultiSelect[T]) numFilteredSelected() int {\n\tvar count int\n\tfor _, o := range m.filteredOptions {\n\t\tif o.selected {\n\t\t\tcount++\n\t\t}\n\t}\n\treturn count\n}\n\nfunc (m *MultiSelect[T]) updateValue() {\n\tvalue := make([]T, 0)\n\tfor _, option := range m.options.val {\n\t\tif option.selected {\n\t\t\tvalue = append(value, option.Value)\n\t\t}\n\t}\n\tm.accessor.Set(value)\n\tm.err = m.validate(m.accessor.Get())\n}\n\nfunc (m *MultiSelect[T]) activeStyles() *FieldStyles {\n\ttheme := m.theme\n\tif theme == nil {\n\t\ttheme = ThemeFunc(ThemeCharm)\n\t}\n\tif m.focused {\n\t\treturn &theme.Theme(m.hasDarkBg).Focused\n\t}\n\treturn &theme.Theme(m.hasDarkBg).Blurred\n}\n\nfunc (m *MultiSelect[T]) titleView() string {\n\tif m.title.val == \"\" {\n\t\treturn \"\"\n\t}\n\tvar (\n\t\tstyles   = m.activeStyles()\n\t\tsb       = strings.Builder{}\n\t\tmaxWidth = m.width - styles.Base.GetHorizontalFrameSize()\n\t)\n\tif m.filtering {\n\t\tsb.WriteString(m.filter.View())\n\t} else if m.filter.Value() != \"\" {\n\t\tsb.WriteString(styles.Title.Render(wrap(m.title.val, maxWidth)))\n\t\tsb.WriteString(styles.Description.Render(\"/\" + m.filter.Value()))\n\t} else {\n\t\tsb.WriteString(styles.Title.Render(wrap(m.title.val, maxWidth)))\n\t}\n\tif m.err != nil {\n\t\tsb.WriteString(styles.ErrorIndicator.String())\n\t}\n\treturn sb.String()\n}\n\nfunc (m *MultiSelect[T]) descriptionView() string {\n\tif m.description.val == \"\" {\n\t\treturn \"\"\n\t}\n\tmaxWidth := m.width - m.activeStyles().Base.GetHorizontalFrameSize()\n\treturn m.activeStyles().Description.Render(wrap(m.description.val, maxWidth))\n}\n\nfunc (m *MultiSelect[T]) renderOption(option Option[T], cursor, selected bool) string {\n\tstyles := m.activeStyles()\n\tvar parts []string\n\tif cursor {\n\t\tparts = append(parts, styles.MultiSelectSelector.String())\n\t} else {\n\t\tparts = append(parts, strings.Repeat(\" \", lipgloss.Width(styles.MultiSelectSelector.String())))\n\t}\n\tif selected {\n\t\tparts = append(parts, styles.SelectedPrefix.String())\n\t\tparts = append(parts, styles.SelectedOption.Render(option.Key))\n\t} else {\n\t\tparts = append(parts, styles.UnselectedPrefix.String())\n\t\tparts = append(parts, styles.UnselectedOption.Render(option.Key))\n\t}\n\treturn lipgloss.JoinHorizontal(lipgloss.Left, parts...)\n}\n\n// cursorLineOffset computes the line offset and height (in lines) for the\n// current cursor position without rendering the full options string.\nfunc (m *MultiSelect[T]) cursorLineOffset() (offset int, height int) {\n\tfor i, option := range m.filteredOptions {\n\t\tline := m.renderOption(option, m.cursor == i, m.filteredOptions[i].selected)\n\t\th := lipgloss.Height(line)\n\t\tif i < m.cursor {\n\t\t\toffset += h\n\t\t}\n\t\tif i == m.cursor {\n\t\t\theight = h\n\t\t\treturn offset, height\n\t\t}\n\t}\n\treturn offset, height\n}\n\nfunc (m *MultiSelect[T]) ensureCursorVisible() {\n\toffset, height := m.cursorLineOffset()\n\tensureVisible(&m.viewport, offset, height)\n}\n\nfunc (m *MultiSelect[T]) optionsView() (string, int, int) {\n\tvar sb strings.Builder\n\n\tif m.options.loading && time.Since(m.options.loadingStart) > spinnerShowThreshold {\n\t\tm.spinner.Style = m.activeStyles().MultiSelectSelector.UnsetString()\n\t\tsb.WriteString(m.spinner.View() + \" Loading...\")\n\t\treturn sb.String(), -1, 1\n\t}\n\n\tvar cursorOffset int\n\tvar cursorHeight int\n\tfor i, option := range m.filteredOptions {\n\t\tcursor := m.cursor == i\n\t\tline := m.renderOption(option, cursor, m.filteredOptions[i].selected)\n\t\tif i < m.cursor {\n\t\t\tcursorOffset += lipgloss.Height(line)\n\t\t}\n\t\tif cursor {\n\t\t\tcursorHeight = lipgloss.Height(line)\n\t\t}\n\t\tsb.WriteString(line)\n\t\tif i < len(m.options.val)-1 {\n\t\t\tsb.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\tfor i := len(m.filteredOptions); i < len(m.options.val)-1; i++ {\n\t\tsb.WriteString(\"\\n\")\n\t}\n\n\treturn sb.String(), cursorOffset, cursorHeight\n}\n\n// View renders the multi-select field.\nfunc (m *MultiSelect[T]) View() string {\n\tstyles := m.activeStyles()\n\n\tvpc, _, _ := m.optionsView()\n\tm.viewport.SetContent(vpc)\n\n\tvar sb strings.Builder\n\tif m.title.val != \"\" || m.title.fn != nil {\n\t\tsb.WriteString(m.titleView())\n\t\tsb.WriteString(\"\\n\")\n\t}\n\tif m.description.val != \"\" || m.description.fn != nil {\n\t\tsb.WriteString(m.descriptionView() + \"\\n\")\n\t}\n\tsb.WriteString(m.viewport.View())\n\treturn styles.Base.Width(m.width).Height(m.height).\n\t\tRender(sb.String())\n}\n\nfunc (m *MultiSelect[T]) printOptions(w io.Writer) {\n\tstyles := m.activeStyles()\n\tvar sb strings.Builder\n\tfor i, option := range m.options.val {\n\t\tif option.selected {\n\t\t\tsb.WriteString(styles.SelectedOption.Render(fmt.Sprintf(\"%d. %s %s\", i+1, \"✓\", option.Key)))\n\t\t} else {\n\t\t\t_, _ = fmt.Fprintf(&sb, \"%d.   %s\", i+1, option.Key)\n\t\t}\n\t\tsb.WriteString(\"\\n\")\n\t}\n\tsb.WriteString(\"0.   Confirm selection\\n\")\n\t_, _ = fmt.Fprint(w, sb.String())\n}\n\n// setFilter sets the filter of the select field.\nfunc (m *MultiSelect[T]) setFilter(filter bool) {\n\tm.filtering = filter\n\tm.keymap.SetFilter.SetEnabled(filter)\n\tm.keymap.Filter.SetEnabled(!filter)\n\tm.keymap.Next.SetEnabled(!filter)\n\tm.keymap.Submit.SetEnabled(!filter)\n\tm.keymap.Prev.SetEnabled(!filter)\n\tm.keymap.ClearFilter.SetEnabled(!filter && m.filter.Value() != \"\")\n}\n\n// filterFunc returns true if the option matches the filter.\nfunc (m *MultiSelect[T]) filterFunc(option string) bool {\n\t// XXX: remove diacritics or allow customization of filter function.\n\treturn strings.Contains(strings.ToLower(option), strings.ToLower(m.filter.Value()))\n}\n\n// setSelectAllHelp enables the appropriate select all or select none keybinding.\nfunc (m *MultiSelect[T]) setSelectAllHelp() {\n\tif m.limit > 0 {\n\t\tm.keymap.SelectAll.SetEnabled(false)\n\t\tm.keymap.SelectNone.SetEnabled(false)\n\t\treturn\n\t}\n\n\tnoneSelected := m.numFilteredSelected() <= 0\n\tallSelected := m.numFilteredSelected() > 0 && m.numFilteredSelected() < len(m.filteredOptions)\n\tselectAll := noneSelected || allSelected\n\tm.keymap.SelectAll.SetEnabled(selectAll)\n\tm.keymap.SelectNone.SetEnabled(!selectAll)\n}\n\n// Run runs the multi-select field.\nfunc (m *MultiSelect[T]) Run() error {\n\treturn Run(m)\n}\n\n// RunAccessible runs the multi-select field in accessible mode.\nfunc (m *MultiSelect[T]) RunAccessible(w io.Writer, r io.Reader) error {\n\tstyles := m.activeStyles()\n\ttitle := styles.Title.\n\t\tPaddingRight(1).\n\t\tRender(cmp.Or(m.title.val, \"Select:\"))\n\t_, _ = fmt.Fprintln(w, title)\n\tlimit := m.limit\n\tif limit == 0 {\n\t\tlimit = len(m.options.val)\n\t}\n\t_, _ = fmt.Fprintf(w, \"Select up to %d options.\\n\", limit)\n\n\tvar choice int\n\tfor {\n\t\tm.printOptions(w)\n\n\t\tprompt := fmt.Sprintf(\"Enter a number between %d and %d: \", 0, len(m.options.val))\n\t\tchoice = accessibility.PromptInt(w, r, prompt, 0, len(m.options.val), nil)\n\t\tif choice <= 0 {\n\t\t\tm.updateValue()\n\t\t\terr := m.validate(m.accessor.Get())\n\t\t\tif err != nil {\n\t\t\t\t_, _ = fmt.Fprintln(w, err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\n\t\tif !m.options.val[choice-1].selected && m.limit > 0 && m.numSelected() >= m.limit {\n\t\t\t_, _ = fmt.Fprintf(w, \"You can't select more than %d options.\\n\", m.limit)\n\t\t\t_, _ = fmt.Fprintln(w)\n\t\t\tcontinue\n\t\t}\n\t\tm.options.val[choice-1].selected = !m.options.val[choice-1].selected\n\t\t_, _ = fmt.Fprintln(w)\n\t}\n\n\treturn nil\n}\n\n// WithTheme sets the theme of the multi-select field.\nfunc (m *MultiSelect[T]) WithTheme(theme Theme) Field {\n\tif m.theme != nil {\n\t\treturn m\n\t}\n\tm.theme = theme\n\tstyles := m.theme.Theme(m.hasDarkBg)\n\n\tst := m.filter.Styles()\n\tst.Cursor.Color = styles.Focused.TextInput.Cursor.GetForeground()\n\tst.Focused.Prompt = styles.Focused.TextInput.Prompt\n\tst.Focused.Text = styles.Focused.TextInput.Text\n\tst.Focused.Placeholder = styles.Focused.TextInput.Placeholder\n\tm.filter.SetStyles(st)\n\n\tm.updateViewportSize()\n\treturn m\n}\n\n// WithKeyMap sets the keymap of the multi-select field.\nfunc (m *MultiSelect[T]) WithKeyMap(k *KeyMap) Field {\n\tm.keymap = k.MultiSelect\n\tif !m.filterable {\n\t\tm.keymap.Filter.SetEnabled(false)\n\t\tm.keymap.ClearFilter.SetEnabled(false)\n\t\tm.keymap.SetFilter.SetEnabled(false)\n\t}\n\treturn m\n}\n\n// WithWidth sets the width of the multi-select field.\nfunc (m *MultiSelect[T]) WithWidth(width int) Field {\n\tm.width = width\n\tm.updateViewportSize()\n\treturn m\n}\n\n// WithHeight sets the total height of the multi-select field. Including padding\n// and help menu heights.\nfunc (m *MultiSelect[T]) WithHeight(height int) Field {\n\tm.Height(height)\n\treturn m\n}\n\n// WithPosition sets the position of the multi-select field.\nfunc (m *MultiSelect[T]) WithPosition(p FieldPosition) Field {\n\tif m.filtering {\n\t\treturn m\n\t}\n\tm.keymap.Prev.SetEnabled(!p.IsFirst())\n\tm.keymap.Next.SetEnabled(!p.IsLast())\n\tm.keymap.Submit.SetEnabled(p.IsLast())\n\treturn m\n}\n\n// GetKey returns the multi-select's key.\nfunc (m *MultiSelect[T]) GetKey() string {\n\treturn m.key\n}\n\n// GetValue returns the multi-select's value.\nfunc (m *MultiSelect[T]) GetValue() any {\n\treturn m.accessor.Get()\n}\n\n// GetFiltering returns whether the multi-select is filtering.\nfunc (m *MultiSelect[T]) GetFiltering() bool {\n\treturn m.filtering\n}\n"
  },
  {
    "path": "field_note.go",
    "content": "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 a note field.\n//\n// A note is responsible for displaying information to the user. Use it to\n// provide context around a different field. Generally, the notes are not\n// interacted with unless the note has a next button `Next(true)`.\ntype Note struct {\n\tid int\n\n\ttitle       Eval[string]\n\tdescription Eval[string]\n\tnextLabel   string\n\n\tfocused        bool\n\tshowNextButton bool\n\tskip           bool\n\n\theight int\n\twidth  int\n\n\ttheme     Theme\n\thasDarkBg bool\n\tkeymap    NoteKeyMap\n}\n\n// NewNote creates a new note field.\n//\n// A note is responsible for displaying information to the user. Use it to\n// provide context around a different field. Generally, the notes are not\n// interacted with unless the note has a next button `Next(true)`.\nfunc NewNote() *Note {\n\treturn &Note{\n\t\tid:             nextID(),\n\t\tshowNextButton: false,\n\t\tskip:           true,\n\t\tnextLabel:      \"Next\",\n\t\ttitle:          Eval[string]{cache: make(map[uint64]string)},\n\t\tdescription:    Eval[string]{cache: make(map[uint64]string)},\n\t}\n}\n\n// Title sets the note field's title.\n//\n// This title will be static, for dynamic titles use `TitleFunc`.\nfunc (n *Note) Title(title string) *Note {\n\tn.title.val = title\n\tn.title.fn = nil\n\treturn n\n}\n\n// TitleFunc sets the title func of the note field.\n//\n// The TitleFunc will be re-evaluated when the binding of the TitleFunc changes.\n// This is useful when you want to display dynamic content and update the title\n// of a note when another part of your form changes.\n//\n// See README.md#Dynamic for more usage information.\nfunc (n *Note) TitleFunc(f func() string, bindings any) *Note {\n\tn.title.fn = f\n\tn.title.bindings = bindings\n\treturn n\n}\n\n// Description sets the note field's description.\n//\n// This description will be static, for dynamic descriptions use `DescriptionFunc`.\nfunc (n *Note) Description(description string) *Note {\n\tn.description.val = description\n\tn.description.fn = nil\n\treturn n\n}\n\n// DescriptionFunc sets the description func of the note field.\n//\n// The DescriptionFunc will be re-evaluated when the binding of the\n// DescriptionFunc changes. This is useful when you want to display dynamic\n// content and update the description of a note when another part of your form\n// changes.\n//\n// For example, you can make a dynamic markdown preview with the following Form & Group.\n//\n//\thuh.NewText().Title(\"Markdown\").Value(&md),\n//\thuh.NewNote().Height(20).Title(\"Preview\").\n//\t  DescriptionFunc(func() string {\n//\t      return md\n//\t  }, &md),\n//\n// Notice the `binding` of the Note is the same as the `Value` of the Text field.\n// This binds the two values together, so that when the `Value` of the Text\n// field changes so does the Note description.\nfunc (n *Note) DescriptionFunc(f func() string, bindings any) *Note {\n\tn.description.fn = f\n\tn.description.bindings = bindings\n\treturn n\n}\n\n// Height sets the note field's height.\nfunc (n *Note) Height(height int) *Note {\n\tn.height = height\n\treturn n\n}\n\n// Next sets whether or not to show the next button.\n//\n//\tTitle\n//\tDescription\n//\n//\t[ Next ]\nfunc (n *Note) Next(show bool) *Note {\n\tn.showNextButton = show\n\treturn n\n}\n\n// NextLabel sets the next button label.\nfunc (n *Note) NextLabel(label string) *Note {\n\tn.nextLabel = label\n\treturn n\n}\n\n// Focus focuses the note field.\nfunc (n *Note) Focus() tea.Cmd {\n\tn.focused = true\n\treturn nil\n}\n\n// Blur blurs the note field.\nfunc (n *Note) Blur() tea.Cmd {\n\tn.focused = false\n\treturn nil\n}\n\n// Error returns the error of the note field.\nfunc (n *Note) Error() error { return nil }\n\n// Skip returns whether the note should be skipped or should be blocking.\nfunc (n *Note) Skip() bool { return n.skip }\n\n// Zoom returns whether the note should be zoomed.\nfunc (n *Note) Zoom() bool { return false }\n\n// KeyBinds returns the help message for the note field.\nfunc (n *Note) KeyBinds() []key.Binding {\n\treturn []key.Binding{\n\t\tn.keymap.Prev,\n\t\tn.keymap.Submit,\n\t\tn.keymap.Next,\n\t}\n}\n\n// Init initializes the note field.\nfunc (n *Note) Init() tea.Cmd { return nil }\n\n// Update updates the note field.\nfunc (n *Note) Update(msg tea.Msg) (Model, tea.Cmd) {\n\tswitch msg := msg.(type) {\n\tcase tea.BackgroundColorMsg:\n\t\tn.hasDarkBg = msg.IsDark()\n\tcase updateFieldMsg:\n\t\tvar cmds []tea.Cmd\n\t\tif ok, hash := n.title.shouldUpdate(); ok {\n\t\t\tn.title.bindingsHash = hash\n\t\t\tif !n.title.loadFromCache() {\n\t\t\t\tn.title.loading = true\n\t\t\t\tcmds = append(cmds, func() tea.Msg {\n\t\t\t\t\treturn updateTitleMsg{id: n.id, title: n.title.fn(), hash: hash}\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t\tif ok, hash := n.description.shouldUpdate(); ok {\n\t\t\tn.description.bindingsHash = hash\n\t\t\tif !n.description.loadFromCache() {\n\t\t\t\tn.description.loading = true\n\t\t\t\tcmds = append(cmds, func() tea.Msg {\n\t\t\t\t\treturn updateDescriptionMsg{id: n.id, description: n.description.fn(), hash: hash}\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t\treturn n, tea.Batch(cmds...)\n\tcase updateTitleMsg:\n\t\tif msg.id == n.id && msg.hash == n.title.bindingsHash {\n\t\t\tn.title.update(msg.title)\n\t\t}\n\tcase updateDescriptionMsg:\n\t\tif msg.id == n.id && msg.hash == n.description.bindingsHash {\n\t\t\tn.description.update(msg.description)\n\t\t}\n\tcase tea.KeyPressMsg:\n\t\tswitch {\n\t\tcase key.Matches(msg, n.keymap.Prev):\n\t\t\treturn n, PrevField\n\t\tcase key.Matches(msg, n.keymap.Next, n.keymap.Submit):\n\t\t\treturn n, NextField\n\t\t}\n\t\treturn n, NextField\n\t}\n\treturn n, nil\n}\n\nfunc (n *Note) activeStyles() *FieldStyles {\n\ttheme := n.theme\n\tif theme == nil {\n\t\ttheme = ThemeFunc(ThemeCharm)\n\t}\n\tif n.focused {\n\t\treturn &theme.Theme(n.hasDarkBg).Focused\n\t}\n\treturn &theme.Theme(n.hasDarkBg).Blurred\n}\n\n// View renders the note field.\nfunc (n *Note) View() string {\n\tstyles := n.activeStyles()\n\tmaxWidth := n.width - styles.Card.GetHorizontalFrameSize()\n\tsb := strings.Builder{}\n\n\tif n.title.val != \"\" || n.title.fn != nil {\n\t\tsb.WriteString(styles.NoteTitle.Render(wrap(n.title.val, maxWidth)))\n\t}\n\tif n.description.val != \"\" || n.description.fn != nil {\n\t\tsb.WriteRune('\\n')\n\t\tsb.WriteString(wrap(render(n.description.val), maxWidth))\n\t\tsb.WriteRune('\\n')\n\t}\n\tif n.showNextButton {\n\t\tsb.WriteRune('\\n')\n\t\tsb.WriteString(styles.Next.Render(n.nextLabel))\n\t}\n\treturn styles.Card.\n\t\tHeight(n.height).\n\t\tWidth(n.width).\n\t\tRender(sb.String())\n}\n\n// Run runs the note field.\nfunc (n *Note) Run() error {\n\treturn Run(n)\n}\n\n// RunAccessible runs an accessible note field.\nfunc (n *Note) RunAccessible(w io.Writer, _ io.Reader) error {\n\tstyles := n.activeStyles()\n\tif n.title.val != \"\" {\n\t\t_, _ = fmt.Fprintln(w, styles.Title.Render(n.title.val))\n\t}\n\tif n.description.val != \"\" {\n\t\t_, _ = fmt.Fprintln(w, n.description.val)\n\t}\n\treturn nil\n}\n\n// WithTheme sets the theme on a note field.\nfunc (n *Note) WithTheme(theme Theme) Field {\n\tif n.theme != nil {\n\t\treturn n\n\t}\n\tn.theme = theme\n\treturn n\n}\n\n// WithKeyMap sets the keymap on a note field.\nfunc (n *Note) WithKeyMap(k *KeyMap) Field {\n\tn.keymap = k.Note\n\treturn n\n}\n\n// WithWidth sets the width of the note field.\nfunc (n *Note) WithWidth(width int) Field {\n\tn.width = width\n\treturn n\n}\n\n// WithHeight sets the height of the note field.\nfunc (n *Note) WithHeight(height int) Field {\n\tn.Height(height)\n\treturn n\n}\n\n// WithPosition sets the position information of the note field.\nfunc (n *Note) WithPosition(p FieldPosition) Field {\n\t// if the note is the only field on the screen,\n\t// we shouldn't skip the entire group.\n\tif p.Field == p.FirstField && p.Field == p.LastField {\n\t\tn.skip = false\n\t}\n\tn.keymap.Prev.SetEnabled(!p.IsFirst())\n\tn.keymap.Next.SetEnabled(!p.IsLast())\n\tn.keymap.Submit.SetEnabled(p.IsLast())\n\treturn n\n}\n\n// GetValue satisfies the Field interface, notes do not have values.\nfunc (n *Note) GetValue() any { return nil }\n\n// GetKey satisfies the Field interface, notes do not have keys.\nfunc (n *Note) GetKey() string { return \"\" }\n\nfunc render(input string) string {\n\tvar result strings.Builder\n\tvar italic, bold, codeblock bool\n\tvar escape bool\n\n\tfor _, char := range input {\n\t\tif escape || codeblock {\n\t\t\tresult.WriteRune(char)\n\t\t\tescape = false\n\t\t\tcontinue\n\t\t}\n\t\tswitch char {\n\t\tcase '\\\\':\n\t\t\tescape = true\n\t\tcase '_':\n\t\t\tif !italic {\n\t\t\t\tresult.WriteString(\"\\033[3m\")\n\t\t\t\titalic = true\n\t\t\t} else {\n\t\t\t\tresult.WriteString(\"\\033[23m\")\n\t\t\t\titalic = false\n\t\t\t}\n\t\tcase '*':\n\t\t\tif !bold {\n\t\t\t\tresult.WriteString(\"\\033[1m\")\n\t\t\t\tbold = true\n\t\t\t} else {\n\t\t\t\tresult.WriteString(\"\\033[22m\")\n\t\t\t\tbold = false\n\t\t\t}\n\t\tcase '`':\n\t\t\tif !codeblock {\n\t\t\t\tresult.WriteString(\"\\033[0;37;40m\")\n\t\t\t\tresult.WriteString(\" \")\n\t\t\t\tcodeblock = true\n\t\t\t} else {\n\t\t\t\tresult.WriteString(\" \")\n\t\t\t\tresult.WriteString(\"\\033[0m\")\n\t\t\t\tcodeblock = false\n\n\t\t\t\tif bold {\n\t\t\t\t\tresult.WriteString(\"\\033[1m\")\n\t\t\t\t}\n\t\t\t\tif italic {\n\t\t\t\t\tresult.WriteString(\"\\033[3m\")\n\t\t\t\t}\n\t\t\t}\n\t\tdefault:\n\t\t\tresult.WriteRune(char)\n\t\t}\n\t}\n\n\t// Reset any open formatting\n\tresult.WriteString(\"\\033[0m\")\n\n\treturn result.String()\n}\n"
  },
  {
    "path": "field_select.go",
    "content": "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/spinner\"\n\t\"charm.land/bubbles/v2/textinput\"\n\t\"charm.land/bubbles/v2/viewport\"\n\ttea \"charm.land/bubbletea/v2\"\n\t\"charm.land/huh/v2/internal/accessibility\"\n\t\"charm.land/lipgloss/v2\"\n\t\"github.com/charmbracelet/x/exp/ordered\"\n)\n\nconst (\n\tminHeight     = 1\n\tdefaultHeight = 10\n)\n\n// Select is a select field.\n//\n// A select field is a field that allows the user to select from a list of\n// options. The options can be provided statically or dynamically using Options\n// or OptionsFunc. The options can be filtered using \"/\" and navigation is done\n// using j/k, up/down, or ctrl+n/ctrl+p keys.\ntype Select[T comparable] struct {\n\tid       int\n\taccessor Accessor[T]\n\tkey      string\n\n\tviewport viewport.Model\n\n\ttitle           Eval[string]\n\tdescription     Eval[string]\n\toptions         Eval[[]Option[T]]\n\tfilteredOptions []Option[T]\n\n\tvalidate func(T) error\n\terr      error\n\n\tselected  int\n\tfocused   bool\n\tfiltering bool\n\tfilter    textinput.Model\n\tspinner   spinner.Model\n\n\tinline    bool\n\twidth     int\n\theight    int\n\ttheme     Theme\n\thasDarkBg bool\n\tkeymap    SelectKeyMap\n}\n\n// NewSelect creates a new select field.\n//\n// A select field is a field that allows the user to select from a list of\n// options. The options can be provided statically or dynamically using Options\n// or OptionsFunc. The options can be filtered using \"/\" and navigation is done\n// using j/k, up/down, or ctrl+n/ctrl+p keys.\nfunc NewSelect[T comparable]() *Select[T] {\n\tfilter := textinput.New()\n\tfilter.Prompt = \"/\"\n\n\ts := spinner.New(spinner.WithSpinner(spinner.Line))\n\n\treturn &Select[T]{\n\t\taccessor:    &EmbeddedAccessor[T]{},\n\t\tvalidate:    func(T) error { return nil },\n\t\tfiltering:   false,\n\t\tfilter:      filter,\n\t\tid:          nextID(),\n\t\toptions:     Eval[[]Option[T]]{cache: make(map[uint64][]Option[T])},\n\t\ttitle:       Eval[string]{cache: make(map[uint64]string)},\n\t\tdescription: Eval[string]{cache: make(map[uint64]string)},\n\t\tspinner:     s,\n\t}\n}\n\n// Value sets the value of the select field.\nfunc (s *Select[T]) Value(value *T) *Select[T] {\n\treturn s.Accessor(NewPointerAccessor(value))\n}\n\n// Accessor sets the accessor of the select field.\nfunc (s *Select[T]) Accessor(accessor Accessor[T]) *Select[T] {\n\ts.accessor = accessor\n\ts.selectValue(s.accessor.Get())\n\ts.updateValue()\n\treturn s\n}\n\nfunc (s *Select[T]) selectValue(value T) {\n\tfor i, o := range s.options.val {\n\t\tif o.Value == value {\n\t\t\ts.selected = i\n\t\t\tbreak\n\t\t}\n\t}\n}\n\n// Key sets the key of the select field which can be used to retrieve the value\n// after submission.\nfunc (s *Select[T]) Key(key string) *Select[T] {\n\ts.key = key\n\treturn s\n}\n\n// Title sets the title of the select field.\n//\n// This title will be static, for dynamic titles use `TitleFunc`.\nfunc (s *Select[T]) Title(title string) *Select[T] {\n\ts.title.val = title\n\ts.title.fn = nil\n\treturn s\n}\n\n// TitleFunc sets the title func of the select field.\n//\n// This TitleFunc will be re-evaluated when the binding of the TitleFunc\n// changes. This when you want to display dynamic content and update the title\n// when another part of your form changes.\n//\n// See README#Dynamic for more usage information.\nfunc (s *Select[T]) TitleFunc(f func() string, bindings any) *Select[T] {\n\ts.title.fn = f\n\ts.title.bindings = bindings\n\treturn s\n}\n\n// Filtering sets the filtering state of the select field.\nfunc (s *Select[T]) Filtering(filtering bool) *Select[T] {\n\ts.filtering = filtering\n\ts.filter.Focus()\n\treturn s\n}\n\n// Description sets the description of the select field.\n//\n// This description will be static, for dynamic descriptions use `DescriptionFunc`.\nfunc (s *Select[T]) Description(description string) *Select[T] {\n\ts.description.val = description\n\treturn s\n}\n\n// DescriptionFunc sets the description func of the select field.\n//\n// This DescriptionFunc will be re-evaluated when the binding of the\n// DescriptionFunc changes. This is useful when you want to display dynamic\n// content and update the description when another part of your form changes.\n//\n// See README#Dynamic for more usage information.\nfunc (s *Select[T]) DescriptionFunc(f func() string, bindings any) *Select[T] {\n\ts.description.fn = f\n\ts.description.bindings = bindings\n\treturn s\n}\n\n// Options sets the options of the select field.\n//\n// This is what your user will select from.\n//\n// Title\n// Description\n//\n//\t-> Option 1\n//\t   Option 2\n//\t   Option 3\n//\n// These options will be static, for dynamic options use `OptionsFunc`.\nfunc (s *Select[T]) Options(options ...Option[T]) *Select[T] {\n\tif len(options) <= 0 {\n\t\treturn s\n\t}\n\ts.options.val = options\n\ts.filteredOptions = options\n\n\ts.selectOption()\n\n\ts.updateViewportSize()\n\ts.updateValue()\n\n\treturn s\n}\n\nfunc (s *Select[T]) selectOption() {\n\t// Set the cursor to the existing value or the last selected option.\n\tfor i, option := range s.options.val {\n\t\tif option.Value == s.accessor.Get() {\n\t\t\ts.selected = i\n\t\t\tbreak\n\t\t}\n\t\tif option.selected {\n\t\t\ts.selected = i\n\t\t\tbreak\n\t\t}\n\t}\n\ts.ensureCursorVisible()\n}\n\n// OptionsFunc sets the options func of the select field.\n//\n// This OptionsFunc will be re-evaluated when the binding of the OptionsFunc\n// changes. This is useful when you want to display dynamic content and update\n// the options when another part of your form changes.\n//\n// For example, changing the state / provinces, based on the selected country.\n//\n//\t   huh.NewSelect[string]().\n//\t\t    Options(huh.NewOptions(\"United States\", \"Canada\", \"Mexico\")...).\n//\t\t    Value(&country).\n//\t\t    Title(\"Country\").\n//\t\t    Height(5),\n//\n//\t\thuh.NewSelect[string]().\n//\t\t  Title(\"State / Province\"). // This can also be made dynamic with `TitleFunc`.\n//\t\t  OptionsFunc(func() []huh.Option[string] {\n//\t\t    s := states[country]\n//\t\t    time.Sleep(1000 * time.Millisecond)\n//\t\t    return huh.NewOptions(s...)\n//\t\t}, &country),\n//\n// See examples/dynamic/dynamic-country/main.go for the full example.\nfunc (s *Select[T]) OptionsFunc(f func() []Option[T], bindings any) *Select[T] {\n\ts.options.fn = f\n\ts.options.bindings = bindings\n\t// If there is no height set, we should attach a static height since these\n\t// options are possibly dynamic.\n\tif s.height <= 0 {\n\t\ts.height = defaultHeight\n\t\ts.updateViewportSize()\n\t}\n\treturn s\n}\n\n// Inline sets whether the select input should be inline.\nfunc (s *Select[T]) Inline(v bool) *Select[T] {\n\ts.inline = v\n\tif v {\n\t\ts.Height(1)\n\t}\n\ts.keymap.Left.SetEnabled(v)\n\ts.keymap.Right.SetEnabled(v)\n\ts.keymap.Up.SetEnabled(!v)\n\ts.keymap.Down.SetEnabled(!v)\n\treturn s\n}\n\n// Height sets the height of the select field. If the number of options exceeds\n// the height, the select field will become scrollable.\nfunc (s *Select[T]) Height(height int) *Select[T] {\n\ts.height = height\n\ts.updateViewportSize()\n\treturn s\n}\n\n// Validate sets the validation function of the select field.\nfunc (s *Select[T]) Validate(validate func(T) error) *Select[T] {\n\ts.validate = validate\n\treturn s\n}\n\n// Error returns the error of the select field.\nfunc (s *Select[T]) Error() error { return s.err }\n\n// Skip returns whether the select should be skipped or should be blocking.\nfunc (*Select[T]) Skip() bool { return false }\n\n// Zoom returns whether the input should be zoomed.\nfunc (*Select[T]) Zoom() bool { return false }\n\n// Focus focuses the select field.\nfunc (s *Select[T]) Focus() tea.Cmd {\n\ts.focused = true\n\treturn nil\n}\n\n// Blur blurs the select field.\nfunc (s *Select[T]) Blur() tea.Cmd {\n\tvalue := s.accessor.Get()\n\tif s.inline {\n\t\ts.clearFilter()\n\t\ts.selectValue(value)\n\t}\n\ts.focused = false\n\ts.err = s.validate(value)\n\treturn nil\n}\n\n// Hovered returns the value of the option under the cursor, and a bool\n// indicating whether one was found. If there are no visible options, returns\n// a zero-valued T and false.\nfunc (s *Select[T]) Hovered() (T, bool) {\n\tif len(s.filteredOptions) == 0 || s.selected >= len(s.filteredOptions) {\n\t\tvar zero T\n\t\treturn zero, false\n\t}\n\treturn s.filteredOptions[s.selected].Value, true\n}\n\n// KeyBinds returns the help keybindings for the select field.\nfunc (s *Select[T]) KeyBinds() []key.Binding {\n\treturn []key.Binding{\n\t\ts.keymap.Up,\n\t\ts.keymap.Down,\n\t\ts.keymap.Left,\n\t\ts.keymap.Right,\n\t\ts.keymap.Filter,\n\t\ts.keymap.SetFilter,\n\t\ts.keymap.ClearFilter,\n\t\ts.keymap.Prev,\n\t\ts.keymap.Next,\n\t\ts.keymap.Submit,\n\t}\n}\n\n// Init initializes the select field.\nfunc (s *Select[T]) Init() tea.Cmd {\n\treturn nil\n}\n\n// Update updates the select field.\nfunc (s *Select[T]) Update(msg tea.Msg) (Model, tea.Cmd) {\n\ts.updateViewportSize()\n\n\tvar cmd tea.Cmd\n\tif s.filtering {\n\t\ts.filter, cmd = s.filter.Update(msg)\n\t}\n\n\tswitch msg := msg.(type) {\n\tcase tea.BackgroundColorMsg:\n\t\ts.hasDarkBg = msg.IsDark()\n\tcase updateFieldMsg:\n\t\tvar cmds []tea.Cmd\n\t\tif ok, hash := s.title.shouldUpdate(); ok {\n\t\t\ts.title.bindingsHash = hash\n\t\t\tif !s.title.loadFromCache() {\n\t\t\t\ts.title.loading = true\n\t\t\t\tcmds = append(cmds, func() tea.Msg {\n\t\t\t\t\treturn updateTitleMsg{id: s.id, title: s.title.fn(), hash: hash}\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t\tif ok, hash := s.description.shouldUpdate(); ok {\n\t\t\ts.description.bindingsHash = hash\n\t\t\tif !s.description.loadFromCache() {\n\t\t\t\ts.description.loading = true\n\t\t\t\tcmds = append(cmds, func() tea.Msg {\n\t\t\t\t\treturn updateDescriptionMsg{id: s.id, description: s.description.fn(), hash: hash}\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t\tif ok, hash := s.options.shouldUpdate(); ok {\n\t\t\ts.clearFilter()\n\t\t\ts.options.bindingsHash = hash\n\t\t\tif s.options.loadFromCache() {\n\t\t\t\ts.filteredOptions = s.options.val\n\t\t\t\ts.selected = ordered.Clamp(s.selected, 0, len(s.options.val)-1)\n\t\t\t} else {\n\t\t\t\ts.options.loading = true\n\t\t\t\ts.options.loadingStart = time.Now()\n\t\t\t\tcmds = append(cmds, func() tea.Msg {\n\t\t\t\t\treturn updateOptionsMsg[T]{id: s.id, hash: hash, options: s.options.fn()}\n\t\t\t\t}, s.spinner.Tick)\n\t\t\t}\n\t\t}\n\t\treturn s, tea.Batch(cmds...)\n\n\tcase spinner.TickMsg:\n\t\tif !s.options.loading {\n\t\t\tbreak\n\t\t}\n\t\ts.spinner, cmd = s.spinner.Update(msg)\n\t\treturn s, cmd\n\n\tcase updateTitleMsg:\n\t\tif msg.id == s.id && msg.hash == s.title.bindingsHash {\n\t\t\ts.title.update(msg.title)\n\t\t}\n\tcase updateDescriptionMsg:\n\t\tif msg.id == s.id && msg.hash == s.description.bindingsHash {\n\t\t\ts.description.update(msg.description)\n\t\t}\n\tcase updateOptionsMsg[T]:\n\t\tif msg.id == s.id && msg.hash == s.options.bindingsHash {\n\t\t\ts.options.update(msg.options)\n\t\t\ts.selectOption()\n\n\t\t\t// since we're updating the options, we need to update the selected\n\t\t\t// cursor position and filteredOptions.\n\t\t\ts.selected = ordered.Clamp(s.selected, 0, len(msg.options)-1)\n\t\t\ts.filteredOptions = msg.options\n\t\t\ts.updateValue()\n\t\t}\n\tcase tea.KeyPressMsg:\n\t\ts.err = nil\n\t\tswitch {\n\t\tcase key.Matches(msg, s.keymap.Filter):\n\t\t\ts.setFiltering(true)\n\t\t\treturn s, s.filter.Focus()\n\t\tcase key.Matches(msg, s.keymap.SetFilter):\n\t\t\tif len(s.filteredOptions) <= 0 {\n\t\t\t\ts.filter.SetValue(\"\")\n\t\t\t\ts.filteredOptions = s.options.val\n\t\t\t}\n\t\t\ts.setFiltering(false)\n\t\tcase key.Matches(msg, s.keymap.ClearFilter):\n\t\t\ts.clearFilter()\n\t\tcase key.Matches(msg, s.keymap.Up, s.keymap.Left):\n\t\t\t// When filtering we should ignore j/k keybindings\n\t\t\t//\n\t\t\t// XXX: Currently, the below check doesn't account for keymap\n\t\t\t// changes. When making this fix it's worth considering ignoring\n\t\t\t// whether to ignore all up/down keybindings as ignoring a-zA-Z0-9\n\t\t\t// may not be enough when international keyboards are considered.\n\t\t\tif s.filtering && (msg.String() == \"k\" || msg.String() == \"h\") {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\ts.selected = s.selected - 1\n\t\t\tif s.selected < 0 {\n\t\t\t\ts.selected = len(s.filteredOptions) - 1\n\t\t\t\ts.viewport.GotoBottom()\n\t\t\t} else {\n\t\t\t\ts.ensureCursorVisible()\n\t\t\t}\n\t\t\ts.updateValue()\n\t\tcase key.Matches(msg, s.keymap.GotoTop):\n\t\t\tif s.filtering {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\ts.selected = 0\n\t\t\ts.viewport.GotoTop()\n\t\t\ts.updateValue()\n\t\tcase key.Matches(msg, s.keymap.GotoBottom):\n\t\t\tif s.filtering {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\ts.selected = len(s.filteredOptions) - 1\n\t\t\ts.viewport.GotoBottom()\n\t\tcase key.Matches(msg, s.keymap.HalfPageUp):\n\t\t\ts.selected = max(s.selected-s.viewport.Height()/2, 0)\n\t\t\ts.ensureCursorVisible()\n\t\t\ts.updateValue()\n\t\tcase key.Matches(msg, s.keymap.HalfPageDown):\n\t\t\ts.selected = min(s.selected+s.viewport.Height()/2, len(s.filteredOptions)-1)\n\t\t\ts.ensureCursorVisible()\n\t\t\ts.updateValue()\n\t\tcase key.Matches(msg, s.keymap.Down, s.keymap.Right):\n\t\t\t// When filtering we should ignore j/k keybindings\n\t\t\t//\n\t\t\t// XXX: See note in the previous case match.\n\t\t\tif s.filtering && (msg.String() == \"j\" || msg.String() == \"l\") {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\ts.selected = s.selected + 1\n\t\t\tif s.selected > len(s.filteredOptions)-1 {\n\t\t\t\ts.selected = 0\n\t\t\t\ts.viewport.GotoTop()\n\t\t\t} else {\n\t\t\t\ts.ensureCursorVisible()\n\t\t\t}\n\t\t\ts.updateValue()\n\t\tcase key.Matches(msg, s.keymap.Prev):\n\t\t\tif s.selected >= len(s.filteredOptions) {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\ts.updateValue()\n\t\t\ts.err = s.validate(s.accessor.Get())\n\t\t\tif s.err != nil {\n\t\t\t\treturn s, nil\n\t\t\t}\n\t\t\ts.updateValue()\n\t\t\treturn s, PrevField\n\t\tcase key.Matches(msg, s.keymap.Next, s.keymap.Submit):\n\t\t\tif s.selected >= len(s.filteredOptions) {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\ts.setFiltering(false)\n\t\t\ts.updateValue()\n\t\t\ts.err = s.validate(s.accessor.Get())\n\t\t\tif s.err != nil {\n\t\t\t\treturn s, nil\n\t\t\t}\n\t\t\ts.updateValue()\n\t\t\treturn s, NextField\n\t\t}\n\n\t\tif s.filtering {\n\t\t\ts.filteredOptions = s.options.val\n\t\t\tif s.filter.Value() != \"\" {\n\t\t\t\ts.filteredOptions = nil\n\t\t\t\tfor _, option := range s.options.val {\n\t\t\t\t\tif s.filterFunc(option.Key) {\n\t\t\t\t\t\ts.filteredOptions = append(s.filteredOptions, option)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif len(s.filteredOptions) > 0 {\n\t\t\t\ts.selected = min(s.selected, len(s.filteredOptions)-1)\n\t\t\t}\n\t\t}\n\n\t\ts.ensureCursorVisible()\n\t}\n\n\treturn s, cmd\n}\n\nfunc (s *Select[T]) updateValue() {\n\tif s.selected < len(s.filteredOptions) && s.selected >= 0 {\n\t\ts.accessor.Set(s.filteredOptions[s.selected].Value)\n\t}\n}\n\n// updateViewportSize updates the viewport size according to the Height setting\n// on this select field.\nfunc (s *Select[T]) updateViewportSize() {\n\tif s.height > 0 {\n\t\tyoffset := 0\n\t\tif ss := s.titleView(); ss != \"\" {\n\t\t\tyoffset += lipgloss.Height(ss)\n\t\t}\n\t\tif ss := s.descriptionView(); ss != \"\" {\n\t\t\tyoffset += lipgloss.Height(ss)\n\t\t}\n\t\ts.viewport.SetHeight(max(minHeight, s.height-yoffset))\n\t\ts.ensureCursorVisible()\n\t} else {\n\t\t// If no height is set size the viewport to the number of options.\n\t\tv, _, _ := s.optionsView()\n\t\ts.viewport.SetHeight(lipgloss.Height(v))\n\t}\n\tif s.width > 0 {\n\t\ts.viewport.SetWidth(s.width)\n\t} else {\n\t\tv, _, _ := s.optionsView()\n\t\ts.viewport.SetWidth(lipgloss.Width(v))\n\t}\n}\n\nfunc (s *Select[T]) activeStyles() *FieldStyles {\n\ttheme := s.theme\n\tif theme == nil {\n\t\ttheme = ThemeFunc(ThemeCharm)\n\t}\n\tif s.focused {\n\t\treturn &theme.Theme(s.hasDarkBg).Focused\n\t}\n\treturn &theme.Theme(s.hasDarkBg).Blurred\n}\n\nfunc (s *Select[T]) titleView() string {\n\tvar (\n\t\tstyles   = s.activeStyles()\n\t\tsb       = strings.Builder{}\n\t\tmaxWidth = s.width - styles.Base.GetHorizontalFrameSize()\n\t)\n\tif s.filtering {\n\t\tsb.WriteString(s.filter.View())\n\t} else if s.filter.Value() != \"\" && !s.inline {\n\t\tsb.WriteString(styles.Description.Render(\"/\" + s.filter.Value()))\n\t} else {\n\t\tsb.WriteString(styles.Title.Render(wrap(s.title.val, maxWidth)))\n\t}\n\tif s.err != nil {\n\t\tsb.WriteString(styles.ErrorIndicator.String())\n\t}\n\treturn sb.String()\n}\n\nfunc (s *Select[T]) descriptionView() string {\n\tif s.description.val == \"\" {\n\t\treturn \"\"\n\t}\n\tmaxWidth := s.width - s.activeStyles().Base.GetHorizontalFrameSize()\n\treturn s.activeStyles().Description.Render(wrap(s.description.val, maxWidth))\n}\n\nfunc (s *Select[T]) optionsView() (string, int, int) {\n\tvar (\n\t\tstyles = s.activeStyles()\n\t\tsb     strings.Builder\n\t)\n\n\tif s.options.loading && time.Since(s.options.loadingStart) > spinnerShowThreshold {\n\t\ts.spinner.Style = s.activeStyles().MultiSelectSelector.UnsetString()\n\t\tsb.WriteString(s.spinner.View() + \" Loading...\")\n\t\treturn sb.String(), -1, 1\n\t}\n\n\tif s.inline {\n\t\toption := styles.TextInput.Placeholder.Render(\"No matches\")\n\t\tif len(s.filteredOptions) > 0 {\n\t\t\toption = styles.SelectedOption.Render(s.filteredOptions[s.selected].Key)\n\t\t}\n\t\treturn lipgloss.NewStyle().\n\t\t\t\tWidth(s.width).\n\t\t\t\tRender(lipgloss.JoinHorizontal(\n\t\t\t\t\tlipgloss.Left,\n\t\t\t\t\tstyles.PrevIndicator.Faint(s.selected <= 0).String(),\n\t\t\t\t\toption,\n\t\t\t\t\tstyles.NextIndicator.Faint(s.selected == len(s.filteredOptions)-1).String(),\n\t\t\t\t)),\n\t\t\t-1, 1\n\t}\n\n\tvar cursorOffset int\n\tvar cursorHeight int\n\tfor i, option := range s.filteredOptions {\n\t\tselected := s.selected == i\n\t\tline := s.renderOption(option, selected)\n\t\tif i < s.selected {\n\t\t\tcursorOffset += lipgloss.Height(line)\n\t\t}\n\t\tif selected {\n\t\t\tcursorHeight = lipgloss.Height(line)\n\t\t}\n\n\t\tsb.WriteString(line)\n\t\tif i < len(s.options.val)-1 {\n\t\t\tsb.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\tfor i := len(s.filteredOptions); i < len(s.options.val)-1; i++ {\n\t\tsb.WriteString(\"\\n\")\n\t}\n\n\treturn sb.String(), cursorOffset, cursorHeight\n}\n\n// cursorLineOffset computes the line offset and height (in lines) for the\n// currently selected option without rendering the full options string.\nfunc (s *Select[T]) cursorLineOffset() (offset int, height int) {\n\tfor i, option := range s.filteredOptions {\n\t\tline := s.renderOption(option, s.selected == i)\n\t\th := lipgloss.Height(line)\n\t\tif i < s.selected {\n\t\t\toffset += h\n\t\t}\n\t\tif i == s.selected {\n\t\t\theight = h\n\t\t\treturn offset, height\n\t\t}\n\t}\n\treturn offset, height\n}\n\n// ensureVisible scrolls a viewport the minimum amount so that the region\n// [offset, offset+height) is within the visible area.\nfunc ensureVisible(vp *viewport.Model, offset, height int) {\n\tif height <= 0 {\n\t\treturn\n\t}\n\tyOff := vp.YOffset()\n\tvHeight := vp.Height()\n\tif offset < yOff {\n\t\tvp.ScrollUp(yOff - offset)\n\t} else if offset+height > yOff+vHeight {\n\t\tvp.ScrollDown(offset + height - yOff - vHeight)\n\t}\n}\n\nfunc (s *Select[T]) ensureCursorVisible() {\n\toffset, height := s.cursorLineOffset()\n\tensureVisible(&s.viewport, offset, height)\n}\n\nfunc (s *Select[T]) renderOption(option Option[T], selected bool) string {\n\tvar (\n\t\tstyles   = s.activeStyles()\n\t\tcursor   = styles.SelectSelector.String()\n\t\tcursorW  = lipgloss.Width(cursor)\n\t\tmaxWidth = s.width - s.activeStyles().Base.GetHorizontalFrameSize() - cursorW\n\t)\n\n\tkey := wrap(option.Key, maxWidth)\n\n\tif selected {\n\t\treturn lipgloss.JoinHorizontal(\n\t\t\tlipgloss.Left,\n\t\t\tcursor,\n\t\t\tstyles.SelectedOption.Render(key),\n\t\t)\n\t}\n\treturn lipgloss.JoinHorizontal(\n\t\tlipgloss.Left,\n\t\tstrings.Repeat(\" \", cursorW),\n\t\tstyles.UnselectedOption.Render(key),\n\t)\n}\n\n// View renders the select field.\nfunc (s *Select[T]) View() string {\n\tstyles := s.activeStyles()\n\tvpc, _, _ := s.optionsView()\n\ts.viewport.SetContent(vpc)\n\n\tvar parts []string\n\tif s.title.val != \"\" || s.title.fn != nil {\n\t\tparts = append(parts, s.titleView())\n\t}\n\tif s.description.val != \"\" || s.description.fn != nil {\n\t\tparts = append(parts, s.descriptionView())\n\t}\n\tparts = append(parts, s.viewport.View())\n\treturn styles.Base.Width(s.width).Height(s.height).\n\t\tRender(strings.Join(parts, \"\\n\"))\n}\n\n// clearFilter clears the value of the filter.\nfunc (s *Select[T]) clearFilter() {\n\ts.filter.SetValue(\"\")\n\ts.filteredOptions = s.options.val\n\ts.setFiltering(false)\n}\n\n// setFiltering sets the filter of the select field.\nfunc (s *Select[T]) setFiltering(filtering bool) {\n\tif s.inline && filtering {\n\t\ts.filter.SetWidth(lipgloss.Width(s.titleView()) - 1 - 1)\n\t}\n\ts.filtering = filtering\n\ts.keymap.SetFilter.SetEnabled(filtering)\n\ts.keymap.Filter.SetEnabled(!filtering)\n\ts.keymap.ClearFilter.SetEnabled(!filtering && s.filter.Value() != \"\")\n}\n\n// filterFunc returns true if the option matches the filter.\nfunc (s *Select[T]) filterFunc(option string) bool {\n\t// XXX: remove diacritics or allow customization of filter function.\n\treturn strings.Contains(strings.ToLower(option), strings.ToLower(s.filter.Value()))\n}\n\n// Run runs the select field.\nfunc (s *Select[T]) Run() error {\n\treturn Run(s)\n}\n\n// RunAccessible runs an accessible select field.\nfunc (s *Select[T]) RunAccessible(w io.Writer, r io.Reader) error {\n\tstyles := s.activeStyles()\n\t_, _ = fmt.Fprintln(w, styles.Title.\n\t\tPaddingRight(1).\n\t\tRender(cmp.Or(s.title.val, \"Select:\")))\n\n\tfor i, option := range s.options.val {\n\t\t_, _ = fmt.Fprintf(w, \"%d. %s\\n\", i+1, option.Key)\n\t}\n\n\tvar defaultValue *int\n\tswitch s.accessor.(type) {\n\tcase *PointerAccessor[T]: // if its of this type, it means it has a default value\n\t\ts.selectOption() // make sure s.selected is set\n\t\tidx := s.selected + 1\n\t\tdefaultValue = &idx\n\t}\n\tprompt := fmt.Sprintf(\"Enter a number between %d and %d: \", 1, len(s.options.val))\n\tif len(s.options.val) == 1 {\n\t\tprompt = \"There is only one option available; enter the number 1:\"\n\t}\n\tfor {\n\t\tchoice := accessibility.PromptInt(w, r, prompt, 1, len(s.options.val), defaultValue)\n\t\toption := s.options.val[choice-1]\n\t\tif err := s.validate(option.Value); err != nil {\n\t\t\t_, _ = fmt.Fprintln(w, err.Error())\n\t\t\t_, _ = fmt.Fprintln(w)\n\t\t\tcontinue\n\t\t}\n\t\ts.accessor.Set(option.Value)\n\t\treturn nil\n\t}\n}\n\n// WithTheme sets the theme of the select field.\nfunc (s *Select[T]) WithTheme(theme Theme) Field {\n\tif s.theme != nil {\n\t\treturn s\n\t}\n\ts.theme = theme\n\tstyles := s.theme.Theme(s.hasDarkBg)\n\n\tst := s.filter.Styles()\n\tst.Cursor.Color = styles.Focused.TextInput.Cursor.GetForeground()\n\tst.Focused.Prompt = styles.Focused.TextInput.Prompt\n\tst.Focused.Text = styles.Focused.TextInput.Text\n\tst.Focused.Placeholder = styles.Focused.TextInput.Placeholder\n\ts.filter.SetStyles(st)\n\n\ts.updateViewportSize()\n\treturn s\n}\n\n// WithKeyMap sets the keymap on a select field.\nfunc (s *Select[T]) WithKeyMap(k *KeyMap) Field {\n\ts.keymap = k.Select\n\ts.keymap.Left.SetEnabled(s.inline)\n\ts.keymap.Right.SetEnabled(s.inline)\n\ts.keymap.Up.SetEnabled(!s.inline)\n\ts.keymap.Down.SetEnabled(!s.inline)\n\treturn s\n}\n\n// WithWidth sets the width of the select field.\nfunc (s *Select[T]) WithWidth(width int) Field {\n\ts.width = width\n\ts.updateViewportSize()\n\treturn s\n}\n\n// WithHeight sets the height of the select field.\nfunc (s *Select[T]) WithHeight(height int) Field {\n\treturn s.Height(height)\n}\n\n// WithPosition sets the position of the select field.\nfunc (s *Select[T]) WithPosition(p FieldPosition) Field {\n\tif s.filtering {\n\t\treturn s\n\t}\n\ts.keymap.Prev.SetEnabled(!p.IsFirst())\n\ts.keymap.Next.SetEnabled(!p.IsLast())\n\ts.keymap.Submit.SetEnabled(p.IsLast())\n\treturn s\n}\n\n// GetKey returns the key of the field.\nfunc (s *Select[T]) GetKey() string { return s.key }\n\n// GetValue returns the value of the field.\nfunc (s *Select[T]) GetValue() any {\n\treturn s.accessor.Get()\n}\n\n// GetFiltering returns the filtering state of the field.\nfunc (s *Select[T]) GetFiltering() bool {\n\treturn s.filtering\n}\n"
  },
  {
    "path": "field_text.go",
    "content": "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.land/bubbles/v2/textarea\"\n\ttea \"charm.land/bubbletea/v2\"\n\t\"charm.land/huh/v2/internal/accessibility\"\n\t\"charm.land/lipgloss/v2\"\n)\n\n// Text is a text field.\n//\n// A text box is responsible for getting multi-line input from the user. Use\n// it to gather longer-form user input. The Text field can be filled with an\n// EDITOR.\ntype Text struct {\n\taccessor Accessor[string]\n\tkey      string\n\tid       int\n\n\ttitle       Eval[string]\n\tdescription Eval[string]\n\tplaceholder Eval[string]\n\n\texternalEditor  bool\n\teditorCmd       string\n\teditorArgs      []string\n\teditorExtension string\n\n\ttextarea textarea.Model\n\n\tfocused  bool\n\tvalidate func(string) error\n\terr      error\n\n\twidth int\n\n\ttheme     Theme\n\thasDarkBg bool\n\tkeymap    TextKeyMap\n}\n\n// NewText creates a new text field.\n//\n// A text box is responsible for getting multi-line input from the user. Use\n// it to gather longer-form user input. The Text field can be filled with an\n// EDITOR.\nfunc NewText() *Text {\n\ttext := textarea.New()\n\ttext.ShowLineNumbers = false\n\ttext.Prompt = \"\"\n\tst := text.Styles()\n\tst.Focused.CursorLine = lipgloss.NewStyle()\n\ttext.SetStyles(st)\n\n\teditorCmd, editorArgs := getEditor()\n\n\tt := &Text{\n\t\taccessor:        &EmbeddedAccessor[string]{},\n\t\tid:              nextID(),\n\t\ttextarea:        text,\n\t\tvalidate:        func(string) error { return nil },\n\t\texternalEditor:  true,\n\t\teditorCmd:       editorCmd,\n\t\teditorArgs:      editorArgs,\n\t\teditorExtension: \"md\",\n\t\ttitle:           Eval[string]{cache: make(map[uint64]string)},\n\t\tdescription:     Eval[string]{cache: make(map[uint64]string)},\n\t\tplaceholder:     Eval[string]{cache: make(map[uint64]string)},\n\t}\n\n\treturn t\n}\n\n// Value sets the value of the text field.\nfunc (t *Text) Value(value *string) *Text {\n\treturn t.Accessor(NewPointerAccessor(value))\n}\n\n// Accessor sets the accessor of the text field.\nfunc (t *Text) Accessor(accessor Accessor[string]) *Text {\n\tt.accessor = accessor\n\tt.textarea.SetValue(t.accessor.Get())\n\treturn t\n}\n\n// Key sets the key of the text field.\nfunc (t *Text) Key(key string) *Text {\n\tt.key = key\n\treturn t\n}\n\n// Title sets the text field's title.\n//\n// This title will be static, for dynamic titles use `TitleFunc`.\nfunc (t *Text) Title(title string) *Text {\n\tt.title.val = title\n\tt.title.fn = nil\n\treturn t\n}\n\n// TitleFunc sets the text field's title func.\n//\n// The TitleFunc will be re-evaluated when the binding of the TitleFunc changes.\n// This is useful when you want to display dynamic content and update the title\n// when another part of your form changes.\n//\n// See README#Dynamic for more usage information.\nfunc (t *Text) TitleFunc(f func() string, bindings any) *Text {\n\tt.title.fn = f\n\tt.title.bindings = bindings\n\treturn t\n}\n\n// Description sets the description of the text field.\n//\n// This description will be static, for dynamic description use `DescriptionFunc`.\nfunc (t *Text) Description(description string) *Text {\n\tt.description.val = description\n\tt.description.fn = nil\n\treturn t\n}\n\n// DescriptionFunc sets the description func of the text field.\n//\n// The DescriptionFunc will be re-evaluated when the binding of the\n// DescriptionFunc changes. This is useful when you want to display dynamic\n// content and update the description when another part of your form changes.\n//\n// See README#Dynamic for more usage information.\nfunc (t *Text) DescriptionFunc(f func() string, bindings any) *Text {\n\tt.description.fn = f\n\tt.description.bindings = bindings\n\treturn t\n}\n\n// Lines sets the number of lines to show of the text field.\nfunc (t *Text) Lines(lines int) *Text {\n\tt.textarea.SetHeight(lines)\n\treturn t\n}\n\n// CharLimit sets the character limit of the text field.\nfunc (t *Text) CharLimit(charlimit int) *Text {\n\tt.textarea.CharLimit = charlimit\n\treturn t\n}\n\n// ShowLineNumbers sets whether or not to show line numbers.\nfunc (t *Text) ShowLineNumbers(show bool) *Text {\n\tt.textarea.ShowLineNumbers = show\n\treturn t\n}\n\n// Placeholder sets the placeholder of the text field.\n//\n// This placeholder will be static, for dynamic placeholders use `PlaceholderFunc`.\nfunc (t *Text) Placeholder(str string) *Text {\n\tt.textarea.Placeholder = str\n\treturn t\n}\n\n// PlaceholderFunc sets the placeholder func of the text field.\n//\n// The PlaceholderFunc will be re-evaluated when the binding of the\n// PlaceholderFunc changes. This is useful when you want to display dynamic\n// content and update the placeholder when another part of your form changes.\n//\n// See README#Dynamic for more usage information.\nfunc (t *Text) PlaceholderFunc(f func() string, bindings any) *Text {\n\tt.placeholder.fn = f\n\tt.placeholder.bindings = bindings\n\treturn t\n}\n\n// Validate sets the validation function of the text field.\nfunc (t *Text) Validate(validate func(string) error) *Text {\n\tt.validate = validate\n\treturn t\n}\n\n// ExternalEditor sets whether option to launch an editor is available.\nfunc (t *Text) ExternalEditor(enabled bool) *Text {\n\tt.externalEditor = enabled\n\treturn t\n}\n\nconst defaultEditor = \"nano\"\n\n// getEditor returns the editor command and arguments.\nfunc getEditor() (string, []string) {\n\teditor := strings.Fields(os.Getenv(\"EDITOR\"))\n\tif len(editor) > 0 {\n\t\treturn editor[0], editor[1:]\n\t}\n\treturn defaultEditor, nil\n}\n\n// Editor specifies which editor to use.\n//\n// The first argument provided is used as the editor command (vim, nvim, nano, etc...)\n// The following (optional) arguments provided are passed as arguments to the editor command.\nfunc (t *Text) Editor(editor ...string) *Text {\n\tif len(editor) > 0 {\n\t\tt.editorCmd = editor[0]\n\t}\n\tif len(editor) > 1 {\n\t\tt.editorArgs = editor[1:]\n\t}\n\treturn t\n}\n\n// EditorExtension specifies arguments to pass into the editor.\nfunc (t *Text) EditorExtension(extension string) *Text {\n\tt.editorExtension = extension\n\treturn t\n}\n\n// Error returns the error of the text field.\nfunc (t *Text) Error() error { return t.err }\n\n// Skip returns whether the textarea should be skipped or should be blocking.\nfunc (*Text) Skip() bool { return false }\n\n// Zoom returns whether the note should be zoomed.\nfunc (*Text) Zoom() bool { return false }\n\n// Focus focuses the text field.\nfunc (t *Text) Focus() tea.Cmd {\n\tt.focused = true\n\treturn t.textarea.Focus()\n}\n\n// Blur blurs the text field.\nfunc (t *Text) Blur() tea.Cmd {\n\tt.focused = false\n\tt.accessor.Set(t.textarea.Value())\n\tt.textarea.Blur()\n\tt.err = t.validate(t.accessor.Get())\n\treturn nil\n}\n\n// KeyBinds returns the help message for the text field.\nfunc (t *Text) KeyBinds() []key.Binding {\n\tt.keymap.Editor.SetEnabled(t.externalEditor)\n\treturn []key.Binding{t.keymap.NewLine, t.keymap.Editor, t.keymap.Prev, t.keymap.Submit, t.keymap.Next}\n}\n\ntype updateValueMsg []byte\n\n// Init initializes the text field.\nfunc (t *Text) Init() tea.Cmd {\n\tt.textarea.Blur()\n\treturn nil\n}\n\n// Update updates the text field.\nfunc (t *Text) Update(msg tea.Msg) (Model, tea.Cmd) {\n\tvar cmds []tea.Cmd\n\tvar cmd tea.Cmd\n\n\tswitch msg := msg.(type) {\n\tcase tea.BackgroundColorMsg:\n\t\tt.hasDarkBg = msg.IsDark()\n\tcase updateValueMsg:\n\t\tt.textarea.SetValue(string(msg))\n\t\tt.textarea, cmd = t.textarea.Update(msg)\n\t\tcmds = append(cmds, cmd)\n\t\tt.accessor.Set(t.textarea.Value())\n\tcase updateFieldMsg:\n\t\tvar cmds []tea.Cmd\n\t\tif ok, hash := t.placeholder.shouldUpdate(); ok {\n\t\t\tt.placeholder.bindingsHash = hash\n\t\t\tif t.placeholder.loadFromCache() {\n\t\t\t\tt.textarea.Placeholder = t.placeholder.val\n\t\t\t} else {\n\t\t\t\tt.placeholder.loading = true\n\t\t\t\tcmds = append(cmds, func() tea.Msg {\n\t\t\t\t\treturn updatePlaceholderMsg{id: t.id, placeholder: t.placeholder.fn(), hash: hash}\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t\tif ok, hash := t.title.shouldUpdate(); ok {\n\t\t\tt.title.bindingsHash = hash\n\t\t\tif !t.title.loadFromCache() {\n\t\t\t\tcmds = append(cmds, func() tea.Msg {\n\t\t\t\t\treturn updateTitleMsg{id: t.id, title: t.title.fn(), hash: hash}\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t\tif ok, hash := t.description.shouldUpdate(); ok {\n\t\t\tt.description.bindingsHash = hash\n\t\t\tif !t.description.loadFromCache() {\n\t\t\t\tt.description.loading = true\n\t\t\t\tcmds = append(cmds, func() tea.Msg {\n\t\t\t\t\treturn updateDescriptionMsg{id: t.id, description: t.description.fn(), hash: hash}\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t\treturn t, tea.Batch(cmds...)\n\tcase updatePlaceholderMsg:\n\t\tif t.id == msg.id && t.placeholder.bindingsHash == msg.hash {\n\t\t\tt.placeholder.update(msg.placeholder)\n\t\t\tt.textarea.Placeholder = msg.placeholder\n\t\t}\n\tcase updateTitleMsg:\n\t\tif t.id == msg.id && t.title.bindingsHash == msg.hash {\n\t\t\tt.title.update(msg.title)\n\t\t}\n\tcase updateDescriptionMsg:\n\t\tif t.id == msg.id && t.description.bindingsHash == msg.hash {\n\t\t\tt.description.update(msg.description)\n\t\t}\n\tcase tea.KeyPressMsg:\n\t\tt.err = nil\n\n\t\tswitch {\n\t\tcase key.Matches(msg, t.keymap.Editor):\n\t\t\text := strings.TrimPrefix(t.editorExtension, \".\")\n\t\t\ttmpFile, _ := os.CreateTemp(os.TempDir(), \"*.\"+ext)\n\t\t\t//nolint:gosec\n\t\t\tcmd := exec.CommandContext(\n\t\t\t\tcontext.TODO(),\n\t\t\t\tt.editorCmd,\n\t\t\t\tappend(t.editorArgs, tmpFile.Name())...,\n\t\t\t)\n\t\t\t_ = os.WriteFile(tmpFile.Name(), []byte(t.textarea.Value()), 0o644) //nolint:mnd,gosec\n\t\t\tcmds = append(cmds, tea.ExecProcess(cmd, func(error) tea.Msg {\n\t\t\t\tcontent, _ := os.ReadFile(tmpFile.Name())\n\t\t\t\t_ = os.Remove(tmpFile.Name())\n\t\t\t\treturn updateValueMsg(content)\n\t\t\t}))\n\t\tcase key.Matches(msg, t.keymap.Next, t.keymap.Submit):\n\t\t\tvalue := t.textarea.Value()\n\t\t\tt.err = t.validate(value)\n\t\t\tif t.err != nil {\n\t\t\t\treturn t, nil\n\t\t\t}\n\t\t\tcmds = append(cmds, NextField)\n\t\tcase key.Matches(msg, t.keymap.Prev):\n\t\t\tvalue := t.textarea.Value()\n\t\t\tt.err = t.validate(value)\n\t\t\tif t.err != nil {\n\t\t\t\treturn t, nil\n\t\t\t}\n\t\t\tcmds = append(cmds, PrevField)\n\t\t}\n\t}\n\n\tt.textarea, cmd = t.textarea.Update(msg)\n\tcmds = append(cmds, cmd)\n\tt.accessor.Set(t.textarea.Value())\n\n\treturn t, tea.Batch(cmds...)\n}\n\nfunc (t *Text) activeStyles() *FieldStyles {\n\ttheme := t.theme\n\tif theme == nil {\n\t\ttheme = ThemeFunc(ThemeCharm)\n\t}\n\tif t.focused {\n\t\treturn &theme.Theme(t.hasDarkBg).Focused\n\t}\n\treturn &theme.Theme(t.hasDarkBg).Blurred\n}\n\n// View renders the text field.\nfunc (t *Text) View() string {\n\tstyles := t.activeStyles()\n\tst := t.textarea.Styles()\n\n\tif t.focused {\n\t\tst.Focused.Placeholder = styles.TextInput.Placeholder\n\t\tst.Focused.Text = styles.TextInput.Text\n\t\tst.Focused.Prompt = styles.TextInput.Prompt\n\t\tst.Focused.CursorLine = styles.TextInput.Text\n\t} else {\n\t\tst.Blurred.Placeholder = styles.TextInput.Placeholder\n\t\tst.Blurred.Text = styles.TextInput.Text\n\t\tst.Blurred.Prompt = styles.TextInput.Prompt\n\t\tst.Blurred.CursorLine = styles.TextInput.Text\n\t}\n\tst.Cursor.Color = styles.TextInput.Cursor.GetBackground()\n\tt.textarea.SetStyles(st)\n\n\tmaxWidth := t.width - styles.Base.GetHorizontalFrameSize()\n\tvar parts []string\n\tif t.title.val != \"\" || t.title.fn != nil {\n\t\theader := styles.Title.Render(wrap(t.title.val, maxWidth))\n\t\tif t.err != nil {\n\t\t\theader += styles.ErrorIndicator.String()\n\t\t}\n\t\tparts = append(parts, header)\n\t}\n\tif t.description.val != \"\" || t.description.fn != nil {\n\t\tparts = append(parts, styles.Description.Render(wrap(t.description.val, maxWidth)))\n\t}\n\tparts = append(parts, t.textarea.View())\n\n\treturn styles.Base.\n\t\tRender(strings.Join(parts, \"\\n\"))\n}\n\n// Run runs the text field.\nfunc (t *Text) Run() error {\n\treturn Run(t)\n}\n\n// RunAccessible runs an accessible text field.\nfunc (t *Text) RunAccessible(w io.Writer, r io.Reader) error {\n\tstyles := t.activeStyles()\n\tprompt := styles.Title.\n\t\tPaddingRight(1).\n\t\tRender(cmp.Or(t.title.val, \"Input:\"))\n\tt.accessor.Set(accessibility.PromptString(\n\t\tw,\n\t\tr,\n\t\tprompt,\n\t\tt.GetValue().(string),\n\t\tfunc(input string) error {\n\t\t\tif err := t.validate(input); err != nil {\n\t\t\t\t// Handle the error from t.validate, return it\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif t.textarea.CharLimit > 0 && len(input) > t.textarea.CharLimit {\n\t\t\t\treturn fmt.Errorf(\"Input cannot exceed %d characters\", t.textarea.CharLimit)\n\t\t\t}\n\t\t\treturn nil\n\t\t},\n\t))\n\treturn nil\n}\n\n// WithTheme sets the theme on a text field.\nfunc (t *Text) WithTheme(theme Theme) Field {\n\tif t.theme != nil {\n\t\treturn t\n\t}\n\tt.theme = theme\n\treturn t\n}\n\n// WithKeyMap sets the keymap on a text field.\nfunc (t *Text) WithKeyMap(k *KeyMap) Field {\n\tt.keymap = k.Text\n\tt.textarea.KeyMap.InsertNewline.SetKeys(t.keymap.NewLine.Keys()...)\n\treturn t\n}\n\n// WithWidth sets the width of the text field.\nfunc (t *Text) WithWidth(width int) Field {\n\tt.width = width\n\tt.textarea.SetWidth(width - t.activeStyles().Base.GetHorizontalFrameSize())\n\treturn t\n}\n\n// WithHeight sets the height of the text field.\nfunc (t *Text) WithHeight(height int) Field {\n\tadjust := 0\n\tif t.title.val != \"\" {\n\t\tadjust++\n\t}\n\tif t.description.val != \"\" {\n\t\tadjust++\n\t}\n\tt.textarea.SetHeight(height - t.activeStyles().Base.GetVerticalFrameSize() - adjust)\n\treturn t\n}\n\n// WithPosition sets the position information of the text field.\nfunc (t *Text) WithPosition(p FieldPosition) Field {\n\tt.keymap.Prev.SetEnabled(!p.IsFirst())\n\tt.keymap.Next.SetEnabled(!p.IsLast())\n\tt.keymap.Submit.SetEnabled(p.IsLast())\n\treturn t\n}\n\n// GetKey returns the key of the field.\nfunc (t *Text) GetKey() string { return t.key }\n\n// GetValue returns the value of the field.\nfunc (t *Text) GetValue() any {\n\treturn t.accessor.Get()\n}\n"
  },
  {
    "path": "form.go",
    "content": "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\"charm.land/bubbles/v2/key\"\n\ttea \"charm.land/bubbletea/v2\"\n\t\"charm.land/huh/v2/internal/compat\"\n\t\"charm.land/huh/v2/internal/selector\"\n)\n\nconst defaultWidth = 80\n\n// Internal ID management. Used during animating to ensure that frame messages\n// are received only by spinner components that sent them.\nvar (\n\tlastID int\n\tidMtx  sync.Mutex\n)\n\n// Return the next ID we should use on the Model.\nfunc nextID() int {\n\tidMtx.Lock()\n\tdefer idMtx.Unlock()\n\tlastID++\n\treturn lastID\n}\n\n// Model is an alias to [compat.Model].\ntype Model = compat.Model\n\n// FormState represents the current state of the form.\ntype FormState int\n\nconst (\n\t// StateNormal is when the user is completing the form.\n\tStateNormal FormState = iota\n\n\t// StateCompleted is when the user has completed the form.\n\tStateCompleted\n\n\t// StateAborted is when the user has aborted the form.\n\tStateAborted\n)\n\n// ErrUserAborted is the error returned when a user exits the form before submitting.\nvar ErrUserAborted = errors.New(\"user aborted\")\n\n// ErrTimeout is the error returned when the timeout is reached.\nvar ErrTimeout = errors.New(\"timeout\")\n\n// ErrTimeoutUnsupported is the error returned when timeout is used while in accessible mode.\nvar ErrTimeoutUnsupported = errors.New(\"timeout is not supported in accessible mode\")\n\n// Form is a collection of groups that are displayed one at a time on a \"page\".\n//\n// The form can navigate between groups and is complete once all the groups are\n// complete.\ntype Form struct {\n\t// collection of groups\n\tselector *selector.Selector[*Group]\n\n\tresults map[string]any\n\n\t// callbacks\n\tSubmitCmd tea.Cmd\n\tCancelCmd tea.Cmd\n\n\tState FormState\n\n\t// whether or not to use bubble tea rendering for accessibility\n\t// purposes, if true, the form will render with basic prompting primitives\n\t// to be more accessible to screen readers.\n\taccessible bool\n\n\tquitting bool\n\taborted  bool\n\n\t// options\n\twidth      int\n\theight     int\n\ttheme      Theme\n\thasDarkBg  bool\n\tkeymap     *KeyMap\n\ttimeout    time.Duration\n\tteaOptions []tea.ProgramOption\n\tviewHook   compat.ViewHook\n\n\tlayout Layout\n\n\t// accessible mode IO\n\toutput io.Writer\n\tinput  io.Reader\n}\n\n// NewForm returns a form with the given groups and default themes and\n// keybindings.\n//\n// Use With* methods to customize the form with options, such as setting\n// different themes and keybindings.\nfunc NewForm(groups ...*Group) *Form {\n\tselector := selector.NewSelector(groups)\n\n\tf := &Form{\n\t\tselector: selector,\n\t\tkeymap:   NewDefaultKeyMap(),\n\t\tresults:  make(map[string]any),\n\t\tlayout:   LayoutDefault,\n\t\tteaOptions: []tea.ProgramOption{\n\t\t\ttea.WithOutput(os.Stderr),\n\t\t},\n\t}\n\n\t// NB: If dynamic forms come into play this will need to be applied when\n\t// groups and fields are added.\n\tf.WithKeyMap(f.keymap)\n\tf.WithWidth(f.width)\n\tf.WithHeight(f.height)\n\tf.UpdateFieldPositions()\n\n\tif os.Getenv(\"TERM\") == \"dumb\" {\n\t\tf.WithWidth(defaultWidth)\n\t\tf.WithAccessible(true)\n\t}\n\n\treturn f\n}\n\n// Field is a primitive of a form.\n//\n// A field represents a single input control on a form such as a text input,\n// confirm button, select option, etc...\n//\n// Each field implements the Bubble Tea Model interface.\ntype Field interface {\n\t// Bubble Tea Model\n\tModel\n\n\t// Bubble Tea Events\n\tBlur() tea.Cmd\n\tFocus() tea.Cmd\n\n\t// Errors and Validation\n\tError() error\n\n\t// Run runs the field individually.\n\tRun() error\n\n\t// RunAccessible runs the field in accessible mode with the given IO.\n\tRunAccessible(w io.Writer, r io.Reader) error\n\n\t// Skip returns whether this input should be skipped or not.\n\tSkip() bool\n\n\t// Zoom returns whether this input should be zoomed or not.\n\t// Zoom allows the field to take focus of the group / form height.\n\tZoom() bool\n\n\t// KeyBinds returns help keybindings.\n\tKeyBinds() []key.Binding\n\n\t// WithTheme sets the theme on a field.\n\tWithTheme(Theme) Field\n\n\t// WithKeyMap sets the keymap on a field.\n\tWithKeyMap(*KeyMap) Field\n\n\t// WithWidth sets the width of a field.\n\tWithWidth(int) Field\n\n\t// WithHeight sets the height of a field.\n\tWithHeight(int) Field\n\n\t// WithPosition tells the field the index of the group and position it is in.\n\tWithPosition(FieldPosition) Field\n\n\t// GetKey returns the field's key.\n\tGetKey() string\n\n\t// GetValue returns the field's value.\n\tGetValue() any\n}\n\n// FieldPosition is positional information about the given field and form.\ntype FieldPosition struct {\n\tGroup      int\n\tField      int\n\tFirstField int\n\tLastField  int\n\tGroupCount int\n\tFirstGroup int\n\tLastGroup  int\n}\n\n// IsFirst returns whether a field is the form's first field.\nfunc (p FieldPosition) IsFirst() bool {\n\treturn p.Field == p.FirstField && p.Group == p.FirstGroup\n}\n\n// IsLast returns whether a field is the form's last field.\nfunc (p FieldPosition) IsLast() bool {\n\treturn p.Field == p.LastField && p.Group == p.LastGroup\n}\n\n// nextGroupMsg is a message to move to the next group.\ntype nextGroupMsg struct{}\n\n// prevGroupMsg is a message to move to the previous group.\ntype prevGroupMsg struct{}\n\n// nextGroup is the command to move to the next group.\nfunc nextGroup() tea.Msg {\n\treturn nextGroupMsg{}\n}\n\n// prevGroup is the command to move to the previous group.\nfunc prevGroup() tea.Msg {\n\treturn prevGroupMsg{}\n}\n\n// WithAccessible sets the form to run in accessible mode to avoid redrawing the\n// views which makes it easier for screen readers to read and describe the form.\n//\n// This avoids using the Bubble Tea renderer and instead simply uses basic\n// terminal prompting to gather input which degrades the user experience but\n// provides accessibility.\nfunc (f *Form) WithAccessible(accessible bool) *Form {\n\tf.accessible = accessible\n\treturn f\n}\n\n// WithShowHelp sets whether or not the form should show help.\n//\n// This allows the form groups and field to show what keybindings are available\n// to the user.\nfunc (f *Form) WithShowHelp(v bool) *Form {\n\tf.selector.Range(func(_ int, group *Group) bool {\n\t\tgroup.WithShowHelp(v)\n\t\treturn true\n\t})\n\treturn f\n}\n\n// WithShowErrors sets whether or not the form should show errors.\n//\n// This allows the form groups and fields to show errors when the Validate\n// function returns an error.\nfunc (f *Form) WithShowErrors(v bool) *Form {\n\tf.selector.Range(func(_ int, group *Group) bool {\n\t\tgroup.WithShowErrors(v)\n\t\treturn true\n\t})\n\treturn f\n}\n\n// WithTheme sets the theme on a form.\n//\n// This allows all groups and fields to be themed consistently, however themes\n// can be applied to each group and field individually for more granular\n// control.\nfunc (f *Form) WithTheme(theme Theme) *Form {\n\tif theme == nil {\n\t\treturn f\n\t}\n\tf.theme = theme\n\tf.selector.Range(func(_ int, group *Group) bool {\n\t\tgroup.WithTheme(theme)\n\t\treturn true\n\t})\n\treturn f\n}\n\n// WithKeyMap sets the keymap on a form.\n//\n// This allows customization of the form key bindings.\nfunc (f *Form) WithKeyMap(keymap *KeyMap) *Form {\n\tif keymap == nil {\n\t\treturn f\n\t}\n\tf.keymap = keymap\n\tf.selector.Range(func(_ int, group *Group) bool {\n\t\tgroup.WithKeyMap(keymap)\n\t\treturn true\n\t})\n\tf.UpdateFieldPositions()\n\treturn f\n}\n\n// WithWidth sets the width of a form.\n//\n// This allows all groups and fields to be sized consistently, however width\n// can be applied to each group and field individually for more granular\n// control.\nfunc (f *Form) WithWidth(width int) *Form {\n\tif width <= 0 {\n\t\treturn f\n\t}\n\tf.width = width\n\tf.selector.Range(func(_ int, group *Group) bool {\n\t\twidth := f.layout.GroupWidth(f, group, width)\n\t\tgroup.WithWidth(width)\n\t\treturn true\n\t})\n\treturn f\n}\n\n// WithHeight sets the height of a form.\nfunc (f *Form) WithHeight(height int) *Form {\n\tif height <= 0 {\n\t\treturn f\n\t}\n\tf.height = height\n\tf.selector.Range(func(_ int, group *Group) bool {\n\t\tgroup.WithHeight(height)\n\t\treturn true\n\t})\n\treturn f\n}\n\n// WithOutput sets the io.Writer to output the form.\n// Default is STDOUT when [Form] is accessible (set with [Form.WithAccessible], STDERR otherwise.\nfunc (f *Form) WithOutput(w io.Writer) *Form {\n\tf.output = w\n\tf.teaOptions = append(f.teaOptions, tea.WithOutput(w))\n\treturn f\n}\n\n// WithInput sets the io.Reader to the input form.\n// Default is STDIN.\nfunc (f *Form) WithInput(r io.Reader) *Form {\n\tf.input = r\n\tf.teaOptions = append(f.teaOptions, tea.WithInput(r))\n\treturn f\n}\n\n// WithTimeout sets the duration for the form to be killed.\nfunc (f *Form) WithTimeout(t time.Duration) *Form {\n\tf.timeout = t\n\treturn f\n}\n\n// WithProgramOptions sets the tea options of the form.\nfunc (f *Form) WithProgramOptions(opts ...tea.ProgramOption) *Form {\n\tf.teaOptions = opts\n\treturn f\n}\n\n// WithViewHook allows to set a [compat.ViewHook].\nfunc (f *Form) WithViewHook(hook compat.ViewHook) *Form {\n\tf.viewHook = hook\n\treturn f\n}\n\n// WithLayout sets the layout on a form.\n//\n// This allows customization of the form group layout.\nfunc (f *Form) WithLayout(layout Layout) *Form {\n\tf.layout = layout\n\treturn f\n}\n\n// UpdateFieldPositions sets the position on all the fields.\nfunc (f *Form) UpdateFieldPositions() *Form {\n\tfirstGroup := 0\n\tlastGroup := f.selector.Total() - 1\n\n\t// determine the first non-hidden group.\n\tf.selector.Range(func(_ int, g *Group) bool {\n\t\tif !f.isGroupHidden(g) {\n\t\t\treturn false\n\t\t}\n\t\tfirstGroup++\n\t\treturn true\n\t})\n\n\t// determine the last non-hidden group.\n\tf.selector.ReverseRange(func(_ int, g *Group) bool {\n\t\tif !f.isGroupHidden(g) {\n\t\t\treturn false\n\t\t}\n\t\tlastGroup--\n\t\treturn true\n\t})\n\n\tf.selector.Range(func(g int, group *Group) bool {\n\t\t// determine the first non-skippable field.\n\t\tvar firstField int\n\t\tgroup.selector.Range(func(_ int, field Field) bool {\n\t\t\tif !field.Skip() || group.selector.Total() == 1 {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tfirstField++\n\t\t\treturn true\n\t\t})\n\n\t\t// determine the last non-skippable field.\n\t\tvar lastField int\n\t\tgroup.selector.ReverseRange(func(i int, field Field) bool {\n\t\t\tlastField = i\n\t\t\tif !field.Skip() || group.selector.Total() == 1 {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn true\n\t\t})\n\n\t\tgroup.selector.Range(func(i int, field Field) bool {\n\t\t\tfield.WithPosition(FieldPosition{\n\t\t\t\tGroup:      g,\n\t\t\t\tField:      i,\n\t\t\t\tFirstField: firstField,\n\t\t\t\tLastField:  lastField,\n\t\t\t\tFirstGroup: firstGroup,\n\t\t\t\tLastGroup:  lastGroup,\n\t\t\t})\n\t\t\treturn true\n\t\t})\n\n\t\treturn true\n\t})\n\treturn f\n}\n\n// Errors returns the current groups' errors.\nfunc (f *Form) Errors() []error {\n\treturn f.selector.Selected().Errors()\n}\n\n// Help returns the current groups' help.\nfunc (f *Form) Help() help.Model {\n\treturn f.selector.Selected().help\n}\n\n// KeyBinds returns the current fields' keybinds.\nfunc (f *Form) KeyBinds() []key.Binding {\n\tgroup := f.selector.Selected()\n\treturn group.selector.Selected().KeyBinds()\n}\n\n// Get returns a result from the form.\nfunc (f *Form) Get(key string) any {\n\treturn f.results[key]\n}\n\n// GetString returns a result as a string from the form.\nfunc (f *Form) GetString(key string) string {\n\tv, ok := f.results[key].(string)\n\tif !ok {\n\t\treturn \"\"\n\t}\n\treturn v\n}\n\n// GetInt returns a result as a int from the form.\nfunc (f *Form) GetInt(key string) int {\n\tv, ok := f.results[key].(int)\n\tif !ok {\n\t\treturn 0\n\t}\n\treturn v\n}\n\n// GetBool returns a result as a string from the form.\nfunc (f *Form) GetBool(key string) bool {\n\tv, ok := f.results[key].(bool)\n\tif !ok {\n\t\treturn false\n\t}\n\treturn v\n}\n\n// NextGroup moves the form to the next group.\nfunc (f *Form) NextGroup() tea.Cmd {\n\t_, cmd := f.Update(nextGroup())\n\treturn cmd\n}\n\n// PrevGroup moves the form to the next group.\nfunc (f *Form) PrevGroup() tea.Cmd {\n\t_, cmd := f.Update(prevGroup())\n\treturn cmd\n}\n\n// NextField moves the form to the next field.\nfunc (f *Form) NextField() tea.Cmd {\n\t_, cmd := f.Update(NextField())\n\treturn cmd\n}\n\n// PrevField moves the form to the next field.\nfunc (f *Form) PrevField() tea.Cmd {\n\t_, cmd := f.Update(PrevField())\n\treturn cmd\n}\n\n// GetFocusedField returns the focused form field.\nfunc (f *Form) GetFocusedField() Field {\n\treturn f.selector.Selected().selector.Selected()\n}\n\n// Init initializes the form.\nfunc (f *Form) Init() tea.Cmd {\n\tvar cmds []tea.Cmd\n\tf.selector.Range(func(i int, group *Group) bool {\n\t\tif i == 0 {\n\t\t\tgroup.active = true\n\t\t}\n\t\tcmds = append(cmds, group.Init())\n\t\treturn true\n\t})\n\n\tif f.isGroupHidden(f.selector.Selected()) {\n\t\tcmds = append(cmds, nextGroup)\n\t}\n\n\tcmds = append(cmds, tea.RequestWindowSize)\n\treturn tea.Sequence(cmds...)\n}\n\n// Update updates the form.\nfunc (f *Form) Update(msg tea.Msg) (Model, tea.Cmd) {\n\t// If the form is aborted or completed there's no need to update it.\n\tif f.State != StateNormal {\n\t\treturn f, nil\n\t}\n\n\tgroup := f.selector.Selected()\n\n\tswitch msg := msg.(type) {\n\tcase tea.BackgroundColorMsg:\n\t\tf.hasDarkBg = msg.IsDark()\n\tcase tea.WindowSizeMsg:\n\t\tif f.width == 0 {\n\t\t\tf.selector.Range(func(_ int, group *Group) bool {\n\t\t\t\twidth := f.layout.GroupWidth(f, group, msg.Width)\n\t\t\t\tgroup.WithWidth(width)\n\t\t\t\treturn true\n\t\t\t})\n\t\t}\n\t\tif f.height == 0 {\n\t\t\t// calculate the needed height, which is the height of the\n\t\t\t// heightest group, accounting for the width, wraps, etc.\n\t\t\tneededHeight := 0\n\t\t\tf.selector.Range(func(_ int, group *Group) bool {\n\t\t\t\tneededHeight = max(neededHeight, group.rawHeight())\n\t\t\t\treturn true\n\t\t\t})\n\n\t\t\tf.selector.Range(func(_ int, group *Group) bool {\n\t\t\t\tgroup.WithHeight(min(neededHeight, msg.Height))\n\t\t\t\treturn true\n\t\t\t})\n\t\t}\n\n\tcase tea.KeyPressMsg:\n\t\tswitch {\n\t\tcase key.Matches(msg, f.keymap.Quit):\n\t\t\tf.aborted = true\n\t\t\tf.quitting = true\n\t\t\tf.State = StateAborted\n\t\t\treturn f, f.CancelCmd\n\t\t}\n\n\tcase nextFieldMsg:\n\t\t// Form is progressing to the next field, let's save the value of the current field.\n\t\tfield := group.selector.Selected()\n\t\tf.results[field.GetKey()] = field.GetValue()\n\n\tcase nextGroupMsg:\n\t\tif len(group.Errors()) > 0 {\n\t\t\treturn f, nil\n\t\t}\n\n\t\tsubmit := func() (Model, tea.Cmd) {\n\t\t\tf.quitting = true\n\t\t\tf.State = StateCompleted\n\t\t\treturn f, f.SubmitCmd\n\t\t}\n\n\t\tif f.selector.OnLast() {\n\t\t\treturn submit()\n\t\t}\n\n\t\tfor i := f.selector.Index() + 1; i < f.selector.Total(); i++ {\n\t\t\tif !f.isGroupHidden(f.selector.Get(i)) {\n\t\t\t\tf.selector.SetIndex(i)\n\t\t\t\tbreak\n\t\t\t}\n\t\t\t// all subsequent groups are hidden, so we must act as\n\t\t\t// if we were in the last one.\n\t\t\tif i == f.selector.Total()-1 {\n\t\t\t\treturn submit()\n\t\t\t}\n\t\t}\n\t\tf.selector.Selected().active = true\n\t\treturn f, f.selector.Selected().Init()\n\n\tcase prevGroupMsg:\n\t\tif len(group.Errors()) > 0 {\n\t\t\treturn f, nil\n\t\t}\n\n\t\tfor i := f.selector.Index() - 1; i >= 0; i-- {\n\t\t\tif !f.isGroupHidden(f.selector.Get(i)) {\n\t\t\t\tf.selector.SetIndex(i)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tf.selector.Selected().active = true\n\t\treturn f, f.selector.Selected().Init()\n\t}\n\n\tm, cmd := group.Update(msg)\n\tf.selector.Set(f.selector.Index(), m.(*Group))\n\n\t// A user input a key, this could hide or show other groups,\n\t// let's update all of their positions.\n\tswitch msg.(type) {\n\tcase tea.KeyPressMsg:\n\t\tf.UpdateFieldPositions()\n\t}\n\n\treturn f, cmd\n}\n\nfunc (f *Form) isGroupHidden(group *Group) bool {\n\thide := group.hide\n\tif hide == nil {\n\t\treturn false\n\t}\n\treturn hide()\n}\n\nfunc (f *Form) getTheme() *Styles {\n\tif f.theme != nil {\n\t\treturn f.theme.Theme(f.hasDarkBg)\n\t}\n\treturn ThemeCharm(f.hasDarkBg)\n}\n\nfunc (f *Form) styles() FormStyles {\n\treturn f.getTheme().Form\n}\n\n// View renders the form.\nfunc (f *Form) View() string {\n\tif f.quitting {\n\t\treturn \"\"\n\t}\n\n\treturn f.styles().Base.Render(f.layout.View(f))\n}\n\n// Run runs the form.\nfunc (f *Form) Run() error {\n\treturn f.RunWithContext(context.Background())\n}\n\n// RunWithContext runs the form with the given context.\nfunc (f *Form) RunWithContext(ctx context.Context) error {\n\tf.SubmitCmd = tea.Quit\n\tf.CancelCmd = tea.Interrupt\n\n\tif f.selector.Total() == 0 {\n\t\treturn nil\n\t}\n\n\tif f.accessible {\n\t\treturn f.runAccessible(\n\t\t\tcmp.Or[io.Writer](f.output, os.Stdout),\n\t\t\tcmp.Or[io.Reader](f.input, os.Stdin),\n\t\t)\n\t}\n\n\treturn f.run(ctx)\n}\n\n// run runs the form in normal mode.\nfunc (f *Form) run(ctx context.Context) error {\n\tvar cancel context.CancelFunc\n\tif f.timeout > 0 {\n\t\tctx, cancel = context.WithTimeout(ctx, f.timeout)\n\t\tdefer cancel()\n\t}\n\n\tf.teaOptions = append(f.teaOptions, tea.WithContext(ctx))\n\tm, err := tea.NewProgram(\n\t\tcompat.ViewModel{\n\t\t\tModel: f,\n\t\t\tViewHook: func(v tea.View) tea.View {\n\t\t\t\tv.ReportFocus = true\n\t\t\t\tif f.viewHook == nil {\n\t\t\t\t\treturn v\n\t\t\t\t}\n\t\t\t\treturn f.viewHook(v)\n\t\t\t},\n\t\t},\n\t\tf.teaOptions...,\n\t).Run()\n\tif m.(compat.ViewModel).Model.(*Form).aborted || errors.Is(err, tea.ErrInterrupted) {\n\t\treturn ErrUserAborted\n\t}\n\tif errors.Is(err, tea.ErrProgramKilled) {\n\t\treturn ErrTimeout\n\t}\n\tif err != nil {\n\t\treturn fmt.Errorf(\"huh: %w\", err)\n\t}\n\treturn nil\n}\n\n// runAccessible runs the form in accessible mode.\nfunc (f *Form) runAccessible(w io.Writer, r io.Reader) error {\n\t// Timeouts are not supported in this mode.\n\tif f.timeout > 0 {\n\t\treturn ErrTimeoutUnsupported\n\t}\n\n\tf.selector.Range(func(_ int, group *Group) bool {\n\t\tgroup.selector.Range(func(_ int, field Field) bool {\n\t\t\tfield.Init()\n\t\t\tfield.Focus()\n\t\t\t_ = field.RunAccessible(w, r)\n\t\t\t_, _ = fmt.Fprintln(w)\n\t\t\treturn true\n\t\t})\n\t\treturn true\n\t})\n\n\treturn nil\n}\n"
  },
  {
    "path": "go.mod",
    "content": "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/lipgloss/v2 v2.0.2\n\tgithub.com/catppuccin/go v0.3.0\n\tgithub.com/charmbracelet/x/ansi v0.11.6\n\tgithub.com/charmbracelet/x/exp/ordered v0.1.0\n\tgithub.com/charmbracelet/x/exp/strings v0.1.0\n\tgithub.com/charmbracelet/x/term v0.2.2\n\tgithub.com/charmbracelet/x/xpty v0.1.3\n\tgithub.com/mitchellh/hashstructure/v2 v2.0.2\n)\n\nrequire (\n\tgithub.com/atotto/clipboard v0.1.4 // indirect\n\tgithub.com/charmbracelet/colorprofile v0.4.2 // indirect\n\tgithub.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect\n\tgithub.com/charmbracelet/x/conpty v0.1.1 // indirect\n\tgithub.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 // indirect\n\tgithub.com/charmbracelet/x/termios v0.1.1 // indirect\n\tgithub.com/charmbracelet/x/windows v0.2.2 // indirect\n\tgithub.com/clipperhouse/displaywidth v0.11.0 // indirect\n\tgithub.com/clipperhouse/uax29/v2 v2.7.0 // indirect\n\tgithub.com/creack/pty v1.1.24 // indirect\n\tgithub.com/dustin/go-humanize v1.0.1 // indirect\n\tgithub.com/lucasb-eyer/go-colorful v1.3.0 // indirect\n\tgithub.com/mattn/go-runewidth v0.0.20 // indirect\n\tgithub.com/muesli/cancelreader v0.2.2 // indirect\n\tgithub.com/rivo/uniseg v0.4.7 // indirect\n\tgithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect\n\tgolang.org/x/sync v0.19.0 // indirect\n\tgolang.org/x/sys v0.42.0 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s=\ncharm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI=\ncharm.land/bubbletea/v2 v2.0.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0=\ncharm.land/bubbletea/v2 v2.0.2/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ=\ncharm.land/lipgloss/v2 v2.0.2 h1:xFolbF8JdpNkM2cEPTfXEcW1p6NRzOWTSamRfYEw8cs=\ncharm.land/lipgloss/v2 v2.0.2/go.mod h1:KjPle2Qd3YmvP1KL5OMHiHysGcNwq6u83MUjYkFvEkM=\ngithub.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=\ngithub.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=\ngithub.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=\ngithub.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=\ngithub.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=\ngithub.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=\ngithub.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=\ngithub.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=\ngithub.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY=\ngithub.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8=\ngithub.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 h1:eyFRbAmexyt43hVfeyBofiGSEmJ7krjLOYt/9CF5NKA=\ngithub.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8/go.mod h1:SQpCTRNBtzJkwku5ye4S3HEuthAlGy2n9VXZnWkEW98=\ngithub.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=\ngithub.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=\ngithub.com/charmbracelet/x/conpty v0.1.1 h1:s1bUxjoi7EpqiXysVtC+a8RrvPPNcNvAjfi4jxsAuEs=\ngithub.com/charmbracelet/x/conpty v0.1.1/go.mod h1:OmtR77VODEFbiTzGE9G1XiRJAga6011PIm4u5fTNZpk=\ngithub.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=\ngithub.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=\ngithub.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=\ngithub.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=\ngithub.com/charmbracelet/x/exp/ordered v0.1.0 h1:55/qLwjIh0gL0Vni+QAWk7T/qRVP6sBf+2agPBgnOFE=\ngithub.com/charmbracelet/x/exp/ordered v0.1.0/go.mod h1:5UHwmG+is5THxMyCJHNPCn2/ecI07aKNrW+LcResjJ8=\ngithub.com/charmbracelet/x/exp/strings v0.1.0 h1:i69S2XI7uG1u4NLGeJPSYU++Nmjvpo9nwd6aoEm7gkA=\ngithub.com/charmbracelet/x/exp/strings v0.1.0/go.mod h1:/ehtMPNh9K4odGFkqYJKpIYyePhdp1hLBRvyY4bWkH8=\ngithub.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=\ngithub.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=\ngithub.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=\ngithub.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=\ngithub.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=\ngithub.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=\ngithub.com/charmbracelet/x/xpty v0.1.3 h1:eGSitii4suhzrISYH50ZfufV3v085BXQwIytcOdFSsw=\ngithub.com/charmbracelet/x/xpty v0.1.3/go.mod h1:poPYpWuLDBFCKmKLDnhBp51ATa0ooD8FhypRwEFtH3Y=\ngithub.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=\ngithub.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=\ngithub.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=\ngithub.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=\ngithub.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=\ngithub.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=\ngithub.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=\ngithub.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=\ngithub.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=\ngithub.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=\ngithub.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=\ngithub.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=\ngithub.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=\ngithub.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=\ngithub.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=\ngithub.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=\ngithub.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=\ngithub.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=\ngithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=\ngithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=\ngolang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=\ngolang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=\ngolang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=\ngolang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=\ngolang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=\ngolang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\n"
  },
  {
    "path": "group.go",
    "content": "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/bubbletea/v2\"\n\t\"charm.land/huh/v2/internal/selector\"\n\t\"charm.land/lipgloss/v2\"\n)\n\n// Group is a collection of fields that are displayed together with a page of\n// the form. While a group is displayed the form completer can switch between\n// fields in the group.\n//\n// If any of the fields in a group have errors, the form will not be able to\n// progress to the next group.\ntype Group struct {\n\t// collection of fields\n\tselector *selector.Selector[Field]\n\n\t// information\n\ttitle       string\n\tdescription string\n\n\t// navigation\n\tviewport viewport.Model\n\n\t// help\n\tshowHelp bool\n\thelp     help.Model\n\n\t// errors\n\tshowErrors bool\n\n\t// group options\n\twidth     int\n\theight    int\n\ttheme     Theme\n\thasDarkBg bool\n\tkeymap    *KeyMap\n\thide      func() bool\n\tactive    bool\n}\n\n// NewGroup returns a new group with the given fields.\nfunc NewGroup(fields ...Field) *Group {\n\tselector := selector.NewSelector(fields)\n\tgroup := &Group{\n\t\tselector:   selector,\n\t\thelp:       help.New(),\n\t\tshowHelp:   true,\n\t\tshowErrors: true,\n\t\tactive:     false,\n\t}\n\n\tgroup.width = 80\n\theight := group.rawHeight()\n\tv := viewport.New(\n\t\tviewport.WithWidth(group.width),\n\t\tviewport.WithHeight(height),\n\t) //nolint:mnd\n\tgroup.viewport = v\n\tgroup.height = height\n\n\treturn group\n}\n\n// Title sets the group's title.\nfunc (g *Group) Title(title string) *Group {\n\tg.title = title\n\treturn g\n}\n\n// Description sets the group's description.\nfunc (g *Group) Description(description string) *Group {\n\tg.description = description\n\treturn g\n}\n\n// WithShowHelp sets whether or not the group's help should be shown.\nfunc (g *Group) WithShowHelp(show bool) *Group {\n\tg.showHelp = show\n\treturn g\n}\n\n// WithShowErrors sets whether or not the group's errors should be shown.\nfunc (g *Group) WithShowErrors(show bool) *Group {\n\tg.showErrors = show\n\treturn g\n}\n\n// WithTheme sets the theme on a group.\nfunc (g *Group) WithTheme(t Theme) *Group {\n\tg.theme = t\n\tg.help.Styles = t.Theme(g.hasDarkBg).Help\n\tg.selector.Range(func(_ int, field Field) bool {\n\t\tfield.WithTheme(t)\n\t\treturn true\n\t})\n\tif g.height <= 0 {\n\t\tg.WithHeight(g.rawHeight())\n\t}\n\treturn g\n}\n\n// WithKeyMap sets the keymap on a group.\nfunc (g *Group) WithKeyMap(k *KeyMap) *Group {\n\tg.keymap = k\n\tg.selector.Range(func(_ int, field Field) bool {\n\t\tfield.WithKeyMap(k)\n\t\treturn true\n\t})\n\treturn g\n}\n\n// WithWidth sets the width on a group.\nfunc (g *Group) WithWidth(width int) *Group {\n\tg.width = width\n\tg.viewport.SetWidth(width)\n\tg.help.SetWidth(width)\n\tg.selector.Range(func(_ int, field Field) bool {\n\t\tfield.WithWidth(width)\n\t\treturn true\n\t})\n\treturn g\n}\n\n// WithHeight sets the height on a group.\nfunc (g *Group) WithHeight(height int) *Group {\n\tg.height = height\n\th := height - g.titleFooterHeight()\n\tg.viewport.SetHeight(h)\n\tg.selector.Range(func(_ int, field Field) bool {\n\t\t// A field height must not exceed the form height.\n\t\tif h < lipgloss.Height(field.View()) {\n\t\t\tfield.WithHeight(h)\n\t\t}\n\t\treturn true\n\t})\n\treturn g\n}\n\n// WithHide sets whether this group should be skipped.\nfunc (g *Group) WithHide(hide bool) *Group {\n\tg.WithHideFunc(func() bool { return hide })\n\treturn g\n}\n\n// WithHideFunc sets the function that checks if this group should be skipped.\nfunc (g *Group) WithHideFunc(hideFunc func() bool) *Group {\n\tg.hide = hideFunc\n\treturn g\n}\n\n// Errors returns the groups' fields' errors.\nfunc (g *Group) Errors() []error {\n\tvar errs []error\n\tg.selector.Range(func(_ int, field Field) bool {\n\t\tif err := field.Error(); err != nil {\n\t\t\terrs = append(errs, err)\n\t\t}\n\t\treturn true\n\t})\n\treturn errs\n}\n\n// updateFieldMsg is a message to update the fields of a group that is currently\n// displayed.\n//\n// This is used to update all TitleFunc, DescriptionFunc, and ...Func update\n// methods to make all fields dynamically update based on user input.\ntype updateFieldMsg struct{}\n\n// nextFieldMsg is a message to move to the next field,\n//\n// each field controls when to send this message such that it is able to use\n// different key bindings or events to trigger group progression.\ntype nextFieldMsg struct{}\n\n// prevFieldMsg is a message to move to the previous field.\n//\n// each field controls when to send this message such that it is able to use\n// different key bindings or events to trigger group progression.\ntype prevFieldMsg struct{}\n\n// NextField is the command to move to the next field.\nfunc NextField() tea.Msg {\n\treturn nextFieldMsg{}\n}\n\n// PrevField is the command to move to the previous field.\nfunc PrevField() tea.Msg {\n\treturn prevFieldMsg{}\n}\n\n// Init initializes the group.\nfunc (g *Group) Init() tea.Cmd {\n\tvar cmds []tea.Cmd\n\n\tcmds = append(cmds, func() tea.Msg { return updateFieldMsg{} })\n\n\tif g.selector.Selected().Skip() {\n\t\tif g.selector.OnLast() {\n\t\t\tcmds = append(cmds, g.prevField()...)\n\t\t} else if g.selector.OnFirst() {\n\t\t\tcmds = append(cmds, g.nextField()...)\n\t\t}\n\t\treturn tea.Batch(cmds...)\n\t}\n\n\tif g.active {\n\t\tcmd := g.selector.Selected().Focus()\n\t\tcmds = append(cmds, cmd)\n\t}\n\tg.buildView()\n\treturn tea.Batch(cmds...)\n}\n\n// nextField moves to the next field.\nfunc (g *Group) nextField() []tea.Cmd {\n\tblurCmd := g.selector.Selected().Blur()\n\tif g.selector.OnLast() {\n\t\treturn []tea.Cmd{blurCmd, nextGroup}\n\t}\n\tg.selector.Next()\n\tfor g.selector.Selected().Skip() {\n\t\tif g.selector.OnLast() {\n\t\t\treturn []tea.Cmd{blurCmd, nextGroup}\n\t\t}\n\t\tg.selector.Next()\n\t}\n\tfocusCmd := g.selector.Selected().Focus()\n\treturn []tea.Cmd{blurCmd, focusCmd}\n}\n\n// prevField moves to the previous field.\nfunc (g *Group) prevField() []tea.Cmd {\n\tblurCmd := g.selector.Selected().Blur()\n\tif g.selector.OnFirst() {\n\t\treturn []tea.Cmd{blurCmd, prevGroup}\n\t}\n\tg.selector.Prev()\n\tfor g.selector.Selected().Skip() {\n\t\tif g.selector.OnFirst() {\n\t\t\treturn []tea.Cmd{blurCmd, prevGroup}\n\t\t}\n\t\tg.selector.Prev()\n\t}\n\tfocusCmd := g.selector.Selected().Focus()\n\treturn []tea.Cmd{blurCmd, focusCmd}\n}\n\n// Update updates the group.\nfunc (g *Group) Update(msg tea.Msg) (Model, tea.Cmd) {\n\tvar cmds []tea.Cmd\n\n\t// Update all the fields in the group.\n\tg.selector.Range(func(i int, field Field) bool {\n\t\tswitch msg := msg.(type) {\n\t\tcase tea.KeyPressMsg, tea.PasteMsg:\n\t\t\tbreak\n\t\tdefault:\n\t\t\tm, cmd := field.Update(msg)\n\t\t\tg.selector.Set(i, m.(Field))\n\t\t\tcmds = append(cmds, cmd)\n\t\t}\n\t\tif g.selector.Index() == i {\n\t\t\tm, cmd := field.Update(msg)\n\t\t\tg.selector.Set(i, m.(Field))\n\t\t\tcmds = append(cmds, cmd)\n\t\t}\n\t\tm, cmd := field.Update(updateFieldMsg{})\n\t\tg.selector.Set(i, m.(Field))\n\t\tcmds = append(cmds, cmd)\n\t\treturn true\n\t})\n\n\tswitch msg := msg.(type) {\n\tcase tea.BackgroundColorMsg:\n\t\tg.hasDarkBg = msg.IsDark()\n\tcase nextFieldMsg:\n\t\tcmds = append(cmds, g.nextField()...)\n\tcase prevFieldMsg:\n\t\tcmds = append(cmds, g.prevField()...)\n\t}\n\n\tg.buildView()\n\n\treturn g, tea.Batch(cmds...)\n}\n\nfunc (g *Group) getTheme() *Styles {\n\tif theme := g.theme; theme != nil {\n\t\treturn theme.Theme(g.hasDarkBg)\n\t}\n\treturn ThemeFunc(ThemeCharm).Theme(g.hasDarkBg)\n}\n\nfunc (g *Group) styles() GroupStyles { return g.getTheme().Group }\n\nfunc (g *Group) getContent() (int, string) {\n\tvar fields strings.Builder\n\toffset := 0\n\n\tgap := g.getTheme().FieldSeparator.Render()\n\n\t// if the focused field is requesting it be zoomed, only show that field.\n\tif g.selector.Selected().Zoom() {\n\t\tg.selector.Selected().WithHeight(g.height)\n\t\tfields.WriteString(g.selector.Selected().View())\n\t} else {\n\t\tg.selector.Range(func(i int, field Field) bool {\n\t\t\tfields.WriteString(field.View())\n\t\t\tif i == g.selector.Index() {\n\t\t\t\toffset = lipgloss.Height(fields.String()) - lipgloss.Height(field.View())\n\t\t\t}\n\t\t\tif i < g.selector.Total()-1 {\n\t\t\t\tfields.WriteString(gap)\n\t\t\t}\n\t\t\treturn true\n\t\t})\n\t}\n\n\treturn offset, fields.String()\n}\n\nfunc (g *Group) buildView() {\n\toffset, content := g.getContent()\n\tg.viewport.SetContent(content)\n\tg.viewport.SetYOffset(offset)\n}\n\n// Header renders the group's header only (no content).\nfunc (g *Group) Header() string {\n\tstyles := g.styles()\n\tvar parts []string\n\tif g.title != \"\" {\n\t\tparts = append(parts, styles.Title.Render(wrap(g.title, g.width)))\n\t}\n\tif g.description != \"\" {\n\t\tparts = append(parts, styles.Description.Render(wrap(g.description, g.width)))\n\t}\n\treturn strings.Join(parts, \"\\n\")\n}\n\n// titleFooterHeight returns the height of the footer + header.\nfunc (g *Group) titleFooterHeight() int {\n\th := 0\n\tif s := g.Header(); s != \"\" {\n\t\th += lipgloss.Height(s)\n\t}\n\tif s := g.Footer(); s != \"\" {\n\t\th += lipgloss.Height(s)\n\t}\n\treturn h\n}\n\n// rawHeight returns the full height of the group, without using a viewport.\nfunc (g *Group) rawHeight() int {\n\treturn lipgloss.Height(g.Content()) + g.titleFooterHeight()\n}\n\n// View renders the group.\nfunc (g *Group) View() string {\n\tvar parts []string\n\tif s := g.Header(); s != \"\" {\n\t\tparts = append(parts, s)\n\t}\n\tparts = append(parts, g.viewport.View())\n\tif s := g.Footer(); s != \"\" {\n\t\t// append an empty line, and the footer (usually the help).\n\t\tparts = append(parts, \"\", s)\n\t}\n\tif len(parts) > 0 {\n\t\t// Trim suffix spaces from the last part as it can accidentally\n\t\t// scroll the view up on some terminals (like Apple's Terminal.app)\n\t\t// when we right to the bottom rightmost corner cell.\n\t\tlastIdx := len(parts) - 1\n\t\tparts[lastIdx] = strings.TrimSuffix(parts[lastIdx], \" \")\n\t}\n\treturn strings.Join(parts, \"\\n\")\n}\n\n// Content renders the group's content only (no footer).\nfunc (g *Group) Content() string {\n\t_, content := g.getContent()\n\treturn content\n}\n\n// Footer renders the group's footer only (no content).\nfunc (g *Group) Footer() string {\n\tvar parts []string\n\terrors := g.Errors()\n\tif g.showHelp && len(errors) <= 0 {\n\t\tparts = append(parts, g.help.ShortHelpView(g.selector.Selected().KeyBinds()))\n\t}\n\tif g.showErrors {\n\t\tfor _, err := range errors {\n\t\t\tparts = append(parts, wrap(\n\t\t\t\tg.getTheme().Focused.ErrorMessage.Render(err.Error()),\n\t\t\t\tg.width,\n\t\t\t))\n\t\t}\n\t}\n\treturn g.styles().Base.\n\t\tRender(strings.Join(parts, \"\\n\"))\n}\n"
  },
  {
    "path": "huh.go",
    "content": "// Package huh provides components to build terminal-based forms and prompts.\npackage huh\n"
  },
  {
    "path": "huh_test.go",
    "content": "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.land/bubbletea/v2\"\n\t\"charm.land/lipgloss/v2\"\n\t\"github.com/charmbracelet/x/ansi\"\n\t\"github.com/charmbracelet/x/xpty\"\n)\n\nconst text = \"Huh\"\n\nvar pretty = lipgloss.NewStyle().\n\tWidth(60).\n\tBorder(lipgloss.NormalBorder()).\n\tMarginTop(1).\n\tPadding(1, 3, 1, 2)\n\nfunc TestForm(t *testing.T) {\n\ttype Taco struct {\n\t\tShell    string\n\t\tBase     string\n\t\tToppings []string\n\t}\n\n\ttype Order struct {\n\t\tTaco         Taco\n\t\tName         string\n\t\tInstructions string\n\t\tDiscount     bool\n\t}\n\n\tvar taco Taco\n\torder := Order{Taco: taco}\n\n\tf := NewForm(\n\t\tNewGroup(\n\t\t\tNewSelect[string]().\n\t\t\t\tOptions(NewOptions(\"Soft\", \"Hard\")...).\n\t\t\t\tTitle(\"Shell?\").\n\t\t\t\tDescription(\"Our tortillas are made fresh in-house every day.\").\n\t\t\t\tValidate(func(t string) error {\n\t\t\t\t\tif t == \"Hard\" {\n\t\t\t\t\t\treturn fmt.Errorf(\"we're out of hard shells, sorry\")\n\t\t\t\t\t}\n\t\t\t\t\treturn nil\n\t\t\t\t}).\n\t\t\t\tValue(&order.Taco.Shell),\n\n\t\t\tNewSelect[string]().\n\t\t\t\tOptions(NewOptions(\"Chicken\", \"Beef\", \"Fish\", \"Beans\")...).\n\t\t\t\tValue(&order.Taco.Base).\n\t\t\t\tTitle(\"Base\"),\n\t\t),\n\n\t\t// Prompt for toppings and special instructions.\n\t\t// The customer can ask for up to 4 toppings.\n\t\tNewGroup(\n\t\t\tNewMultiSelect[string]().\n\t\t\t\tTitle(\"Toppings\").\n\t\t\t\tDescription(\"Choose up to 4.\").\n\t\t\t\tOptions(\n\t\t\t\t\tNewOption(\"Lettuce\", \"lettuce\").Selected(true),\n\t\t\t\t\tNewOption(\"Tomatoes\", \"tomatoes\").Selected(true),\n\t\t\t\t\tNewOption(\"Corn\", \"corn\"),\n\t\t\t\t\tNewOption(\"Salsa\", \"salsa\"),\n\t\t\t\t\tNewOption(\"Sour Cream\", \"sour cream\"),\n\t\t\t\t\tNewOption(\"Cheese\", \"cheese\"),\n\t\t\t\t).\n\t\t\t\tValidate(func(t []string) error {\n\t\t\t\t\tif len(t) <= 0 {\n\t\t\t\t\t\treturn fmt.Errorf(\"at least one topping is required\")\n\t\t\t\t\t}\n\t\t\t\t\treturn nil\n\t\t\t\t}).\n\t\t\t\tValue(&order.Taco.Toppings).\n\t\t\t\tFilterable(true).\n\t\t\t\tLimit(4),\n\t\t),\n\n\t\t// Gather final details for the order.\n\t\tNewGroup(\n\t\t\tNewInput().\n\t\t\t\tValue(&order.Name).\n\t\t\t\tTitle(\"What's your name?\").\n\t\t\t\tPlaceholder(\"Margaret Thatcher\").\n\t\t\t\tDescription(\"For when your order is ready.\"),\n\n\t\t\tNewText().\n\t\t\t\tValue(&order.Instructions).\n\t\t\t\tPlaceholder(\"Just put it in the mailbox please\").\n\t\t\t\tTitle(\"Special Instructions\").\n\t\t\t\tDescription(\"Anything we should know?\").\n\t\t\t\tCharLimit(400),\n\n\t\t\tNewConfirm().\n\t\t\t\tTitle(\"Would you like 15% off?\").\n\t\t\t\tValue(&order.Discount).\n\t\t\t\tAffirmative(\"Yes!\").\n\t\t\t\tNegative(\"No.\"),\n\t\t),\n\t)\n\n\tf.Update(f.Init())\n\n\tview := viewModel(f)\n\n\t//\n\t//  ┃ Shell?\n\t//  ┃ Our tortillas are made fresh in-house every day.\n\t//  ┃ > Soft\n\t//  ┃   Hard\n\t//\n\t//    Base\n\t//    > Chicken\n\t//      Beef\n\t//      Fish\n\t//      Beans\n\t//\n\t//   ↑ up • ↓ down • / filter • enter select\n\t//\n\n\tif !strings.Contains(view, \"┃ Shell?\") {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Error(\"Expected form to contain Shell? title\")\n\t}\n\n\tif !strings.Contains(view, \"Our tortillas are made fresh in-house every day.\") {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Error(\"Expected form to contain tortilla description\")\n\t}\n\n\tif !strings.Contains(view, \"Base\") {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Error(\"Expected form to contain Base title\")\n\t}\n\n\t// Attempt to select hard shell and retrieve error.\n\tm := batchUpdate(f.Update(keypress('j')))\n\tm = batchUpdate(m.Update(codeKeypress(tea.KeyTab)))\n\tview = viewModel(m)\n\n\tif !strings.Contains(view, \"* we're out of hard shells, sorry\") {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Error(\"Expected form to show out of hard shells error\")\n\t}\n\n\t// select back the soft shell\n\tm = batchUpdate(m.Update(keypress('k')))\n\tm = batchUpdate(m.Update(codeKeypress(tea.KeyEnter)))\n\n\tview = viewModel(m)\n\n\tif !strings.Contains(view, \"┃ > Chicken\") {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Fatal(\"Expected form to continue to base group\")\n\t}\n\n\t// batchMsg + nextGroup\n\tm = batchUpdate(m.Update(codeKeypress(tea.KeyEnter)))\n\tview = viewModel(m)\n\n\t//\n\t// ┃ Toppings\n\t// ┃ Choose up to 4.\n\t// ┃ > ✓ Lettuce\n\t// ┃   ✓ Tomatoes\n\t// ┃   • Corn\n\t// ┃   • Salsa\n\t// ┃   • Sour Cream\n\t// ┃   • Cheese\n\t//\n\t//  x toggle • ↑ up • ↓ down • enter confirm • shift+tab back\n\t//\n\tif !strings.Contains(view, \"Toppings\") {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Fatal(\"Expected form to show toppings group\")\n\t}\n\n\tif !strings.Contains(view, \"Choose up to 4.\") {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Error(\"Expected form to show toppings description\")\n\t}\n\n\tif !strings.Contains(view, \"> ✓ Lettuce \") {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Error(\"Expected form to preselect lettuce\")\n\t}\n\n\tif !strings.Contains(view, \"  ✓ Tomatoes\") {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Error(\"Expected form to preselect tomatoes\")\n\t}\n\n\tm = batchUpdate(m.Update(keypress('j')))\n\tm = batchUpdate(m.Update(keypress('j')))\n\tview = viewModel(m)\n\n\tif !strings.Contains(view, \"> • Corn\") {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Error(\"Expected form to change selection to corn\")\n\t}\n\n\tm = batchUpdate(m.Update(keypress('x')))\n\tview = viewModel(m)\n\n\tif !strings.Contains(view, \"> ✓ Corn\") {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Error(\"Expected form to change selection to corn\")\n\t}\n\n\tm = batchUpdate(m.Update(codeKeypress(tea.KeyEnter)))\n\tview = viewModel(m)\n\n\tif !strings.Contains(view, \"What's your name?\") {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Error(\"Expected form to prompt for name\")\n\t}\n\n\tif !strings.Contains(view, \"Special Instructions\") {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Error(\"Expected form to prompt for special instructions\")\n\t}\n\n\tif !strings.Contains(view, \"Would you like 15% off?\") {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Error(\"Expected form to prompt for discount\")\n\t}\n\n\t//\n\t// ┃ What's your name?\n\t// ┃ For when your order is ready.\n\t// ┃ > Margaret Thatcher\n\t//\n\t//    Special Instructions\n\t//    Anything we should know?\n\t//    Just put it in the mailbox please\n\t//\n\t//    Would you like 15% off?\n\t//\n\t//      Yes!     No.\n\t//\n\t//   enter next • shift+tab back\n\t//\n\ttypeText(m, \"Glen\")\n\tview = viewModel(m)\n\tif !strings.Contains(view, \"Glen\") {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Error(\"Expected form to accept user input\")\n\t}\n\n\tif order.Taco.Shell != \"Soft\" {\n\t\tt.Error(\"Expected order shell to be Soft\")\n\t}\n\n\tif order.Taco.Base != \"Chicken\" {\n\t\tt.Error(\"Expected order shell to be Chicken\")\n\t}\n\n\tif len(order.Taco.Toppings) != 3 {\n\t\tt.Error(\"Expected order to have 3 toppings\")\n\t}\n\n\tif order.Name != \"Glen\" {\n\t\tt.Error(\"Expected order name to be Glen\")\n\t}\n\n\t// TODO: Finish and submit form.\n}\n\nfunc TestInput(t *testing.T) {\n\tfield := NewInput()\n\tf := NewForm(NewGroup(field))\n\tf.Update(f.Init())\n\n\tview := viewModel(f)\n\n\tif !strings.Contains(view, \">\") {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Error(\"Expected field to contain prompt.\")\n\t}\n\n\t// Type Huh in the form.\n\tf = typeText(f, text)\n\tview = viewModel(f)\n\n\tif !strings.Contains(view, text) {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Error(\"Expected field to contain \" + text)\n\t}\n\n\tif !strings.Contains(view, \"enter submit\") {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Error(\"Expected field to contain help.\")\n\t}\n\n\tif field.GetValue() != text {\n\t\tt.Error(\"Expected field value to be \" + text)\n\t}\n}\n\nfunc TestPasteNotDuplicated(t *testing.T) {\n\tfield := NewInput().Title(\"Name\")\n\tf := NewForm(NewGroup(field))\n\tf.Update(f.Init())\n\n\tf = batchUpdate(f.Update(tea.PasteMsg{Content: \"hello\"})).(*Form)\n\n\tif field.GetValue() != \"hello\" {\n\t\tt.Errorf(\"Expected field value to be %q, got %q (paste was duplicated)\", \"hello\", field.GetValue())\n\t}\n}\n\nfunc TestInlineInput(t *testing.T) {\n\tfield := NewInput().\n\t\tTitle(\"Input \").\n\t\tPrompt(\": \").\n\t\tDescription(\"Description\").\n\t\tInline(true)\n\n\tf := NewForm(NewGroup(field)).WithWidth(40)\n\tf.Update(f.Init())\n\n\tview := viewModel(f)\n\n\tif !strings.Contains(view, \"┃ Input Description:\") {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Error(\"Expected field to contain inline input.\")\n\t}\n\n\t// Type Huh in the form.\n\tf = typeText(f, text)\n\tview = viewModel(f)\n\n\tif !strings.Contains(view, text) {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Error(\"Expected field to contain \" + text)\n\t}\n\n\tif !strings.Contains(view, \"enter submit\") {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Error(\"Expected field to contain help.\")\n\t}\n\n\tif !strings.Contains(view, \"┃ Input Description: \"+text) {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Error(\"Expected field to contain help.\")\n\t}\n\n\tif field.GetValue() != text {\n\t\tt.Error(\"Expected field value to be \" + text)\n\t}\n}\n\nfunc TestText(t *testing.T) {\n\tfield := NewText()\n\tf := NewForm(NewGroup(field))\n\tf.Update(f.Init())\n\n\t// Type Huh in the form.\n\tf = typeText(f, text)\n\tview := viewModel(f)\n\n\tif !strings.Contains(view, text) {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Error(\"Expected field to contain \" + text)\n\t}\n\n\tif !strings.Contains(view, \"alt+enter / ctrl+j new line • ctrl+e open editor • enter submit\") {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Error(\"Expected field to contain help.\")\n\t}\n\n\tif field.GetValue() != text {\n\t\tt.Error(\"Expected field value to be \" + text)\n\t}\n}\n\nfunc TestTextExternalEditorHidden(t *testing.T) {\n\tfield := NewText().ExternalEditor(false)\n\tf := NewForm(NewGroup(field))\n\tf.Update(f.Init())\n\n\t// Type Huh in the form.\n\tf = typeText(f, text)\n\tview := viewModel(f)\n\n\tif !strings.Contains(view, text) {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Error(\"Expected field to contain \" + text)\n\t}\n\n\tif strings.Contains(view, \"ctrl+e open editor\") {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Error(\"Expected field to contain help without ctrl+e.\")\n\t}\n\n\tif field.GetValue() != text {\n\t\tt.Error(\"Expected field value to be \" + text)\n\t}\n}\n\nfunc TestConfirm(t *testing.T) {\n\tfield := NewConfirm().Title(\"Are you sure?\")\n\tf := NewForm(NewGroup(field))\n\tf.Update(f.Init())\n\tview := viewModel(f)\n\n\tif !strings.Contains(view, \"Yes\") {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Error(\"Expected field to contain Yes.\")\n\t}\n\n\tif !strings.Contains(view, \"No\") {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Error(\"Expected field to contain No.\")\n\t}\n\n\tif !strings.Contains(view, \"Are you sure?\") {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Error(\"Expected field to contain Are you sure?.\")\n\t}\n\n\tif !strings.Contains(view, \"←/→ toggle • enter submit\") {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Error(\"Expected field to contain help.\")\n\t}\n\n\tif field.GetValue() != false {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Error(\"Expected field value to be false\")\n\t}\n\n\t// Toggle left\n\tf.Update(codeKeypress(tea.KeyLeft))\n\n\tif field.GetValue() != true {\n\t\tt.Error(\"Expected field value to be true\")\n\t}\n\n\t// Toggle right\n\tf.Update(codeKeypress(tea.KeyRight))\n\n\tif field.GetValue() != false {\n\t\tt.Error(\"Expected field value to be false\")\n\t}\n}\n\nfunc TestSelect(t *testing.T) {\n\tfield := NewSelect[string]().\n\t\tOptions(NewOptions(\n\t\t\t\"Foo\\nLine 2\",\n\t\t\t\"Bar\\nLine 2\",\n\t\t\t\"Baz\\nLine 2\",\n\t\t\t\"Ban\\nLine 2\",\n\t\t)...).\n\t\tTitle(\"Which one?\")\n\tf := NewForm(NewGroup(field)).WithHeight(5)\n\tf.Update(f.Init())\n\n\tview := viewModel(f)\n\n\tif !strings.Contains(view, \"Foo\") {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Error(\"Expected field to contain Foo.\")\n\t}\n\n\tif !strings.Contains(view, \"Which one?\") {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Error(\"Expected field to contain Which one?.\")\n\t}\n\n\tif !strings.Contains(view, \"> Foo\") {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Error(\"Expected cursor to be on Foo.\")\n\t}\n\n\t// Move selection cursor down\n\tf = batchUpdate(f.Update(codeKeypress(tea.KeyDown))).(*Form)\n\n\tview = viewModel(f)\n\n\tif got, ok := field.Hovered(); !ok || got != \"Bar\\nLine 2\" {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Error(\"Expected cursor to be on Bar.\")\n\t}\n\n\tif strings.Contains(view, \"> Foo\") {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Error(\"Expected cursor to be on Bar.\")\n\t}\n\n\tif !strings.Contains(view, \"> Bar\") {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Error(\"Expected cursor to be on Bar.\")\n\t}\n\n\tif !strings.Contains(view, \"↑ up • ↓ down • / filter • enter submit\") {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Error(\"Expected field to contain help.\")\n\t}\n\n\t// Submit\n\tf.Update(codeKeypress(tea.KeyEnter))\n\n\tif field.GetValue() != \"Bar\\nLine 2\" {\n\t\tt.Error(\"Expected field value to be Bar\")\n\t}\n}\n\n// doAllUpdates updates the form with the given command, then continues updating it with any resultant commands from the update until no more are returned.\nfunc doAllUpdates(f *Form, cmd tea.Cmd) {\n\tif cmd == nil {\n\t\treturn\n\t}\n\tvar cmds []tea.Cmd\n\tswitch msg := cmd().(type) {\n\tcase tea.BatchMsg:\n\t\tfor _, subcommand := range msg {\n\t\t\tdoAllUpdates(f, subcommand)\n\t\t}\n\t\treturn\n\tdefault:\n\t\t_, result := f.Update(msg)\n\t\tcmds = append(cmds, result)\n\t}\n\tdoAllUpdates(f, tea.Batch(cmds...))\n}\n\nfunc TestSelectDynamic(t *testing.T) {\n\ttrigger := \"initial\"\n\n\tfield1 := NewSelect[string]().\n\t\tTitleFunc(func() string {\n\t\t\treturn \"field1 title \" + trigger\n\t\t}, &trigger).\n\t\tDescriptionFunc(func() string {\n\t\t\treturn \"field1 desc \" + trigger\n\t\t}, &trigger).\n\t\tOptionsFunc(func() []Option[string] {\n\t\t\treturn []Option[string]{NewOption(\"field1 opt \"+trigger, \"field1 opt \"+trigger)}\n\t\t}, &trigger)\n\tfield2 := NewSelect[string]().\n\t\tTitleFunc(func() string {\n\t\t\treturn \"field2 title \" + trigger\n\t\t}, &trigger).\n\t\tDescriptionFunc(func() string {\n\t\t\treturn \"field2 desc \" + trigger\n\t\t}, &trigger).\n\t\tOptionsFunc(func() []Option[string] {\n\t\t\treturn []Option[string]{NewOption(\"field2 opt \"+trigger, \"field2 opt \"+trigger)}\n\t\t}, &trigger)\n\tfield1.WithHeight(5)\n\tfield2.WithHeight(5)\n\tf := NewForm(NewGroup(field1, field2)).WithHeight(10)\n\n\tdoAllUpdates(f, f.Init())\n\n\tview := viewModel(f)\n\n\texpectedStrings := []string{\n\t\t\"field1 title initial\",\n\t\t\"field1 desc initial\",\n\t\t\"field1 opt initial\",\n\t\t\"field2 title initial\",\n\t\t\"field2 desc initial\",\n\t\t\"field2 opt initial\",\n\t}\n\tfor _, expected := range expectedStrings {\n\t\tif !strings.Contains(view, expected) {\n\t\t\tt.Log(pretty.Render(view))\n\t\t\tt.Error(\"Expected view to contain \" + expected)\n\t\t}\n\t}\n\n\tif field1.GetValue() != \"field1 opt initial\" {\n\t\tt.Errorf(\"Expected field1 value to be field1 opt initial but was %s\", field1.GetValue())\n\t}\n\tif field2.GetValue() != \"field2 opt initial\" {\n\t\tt.Errorf(\"Expected field2 value to be field2 opt initial but was %s\", field2.GetValue())\n\t}\n\n\ttrigger = \"updated\"\n\t_, cmd := f.Update(nil)\n\tdoAllUpdates(f, cmd)\n\tview = viewModel(f)\n\n\texpectedStrings = []string{\n\t\t\"field1 title updated\",\n\t\t\"field1 desc updated\",\n\t\t\"field1 opt updated\",\n\t\t\"field2 title updated\",\n\t\t\"field2 desc updated\",\n\t\t\"field2 opt updated\",\n\t}\n\tfor _, expected := range expectedStrings {\n\t\tif !strings.Contains(view, expected) {\n\t\t\tt.Log(pretty.Render(view))\n\t\t\tt.Error(\"Expected view to contain \" + expected)\n\t\t}\n\t}\n\n\tif field1.GetValue() != \"field1 opt updated\" {\n\t\tt.Errorf(\"Expected field1 value to be field1 opt updated but was %s\", field1.GetValue())\n\t}\n\tif field2.GetValue() != \"field2 opt updated\" {\n\t\tt.Errorf(\"Expected field2 value to be field2 opt updated but was %s\", field1.GetValue())\n\t}\n}\n\nfunc TestMultiSelect(t *testing.T) {\n\tfield := NewMultiSelect[string]().\n\t\tOptions(NewOptions(\n\t\t\t\"Foo\\nLine2\",\n\t\t\t\"Bar\\nLine2\",\n\t\t\t\"Baz\\nLine2\",\n\t\t\t\"Ban\\nLine2\",\n\t\t)...).\n\t\tTitle(\"Which one?\")\n\tf := NewForm(NewGroup(field)).\n\t\tWithHeight(5)\n\tf.Update(f.Init())\n\n\tview := viewModel(f)\n\n\tif !strings.Contains(view, \"Foo\") {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Error(\"Expected field to contain Foo.\")\n\t}\n\n\tif !strings.Contains(view, \"Which one?\") {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Error(\"Expected field to contain Which one?.\")\n\t}\n\n\tif !strings.Contains(view, \"> • Foo\") {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Error(\"Expected cursor to be on Foo.\")\n\t}\n\n\t// Move selection cursor down\n\tm := batchUpdate(f.Update(keypress('j')))\n\tview = viewModel(m)\n\n\tif got, ok := field.Hovered(); !ok || got != \"Bar\\nLine2\" {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Error(\"Expected cursor to be on Bar.\")\n\t}\n\n\tif strings.Contains(view, \"> • Foo\") {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Error(\"Expected cursor to be on Bar.\")\n\t}\n\n\tif !strings.Contains(view, \"> • Bar\") {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Error(\"Expected cursor to be on Bar.\")\n\t}\n\n\t// Toggle\n\tm = batchUpdate(f.Update(keypress('x')))\n\tview = viewModel(m)\n\n\tif !strings.Contains(view, \"> ✓ Bar\") {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Error(\"Expected cursor to be on Bar.\")\n\t}\n\n\tif !strings.Contains(view, \"x toggle • ↑ up • ↓ down • / filter • enter submit\") {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Error(\"Expected field to contain help.\")\n\t}\n\n\t// Submit\n\tf.Update(codeKeypress(tea.KeyEnter))\n\n\tvalue := field.GetValue()\n\tv, ok := value.([]string)\n\tif !ok {\n\t\tt.Error(\"Expected field value to a slice of string\")\n\t\treturn\n\t}\n\tif len(v) != 1 {\n\t\tt.Error(\"Expected field value length to be 1\")\n\t} else {\n\t\tif v[0] != \"Bar\\nLine2\" {\n\t\t\tt.Error(\"Expected first field value to be Bar\")\n\t\t}\n\t}\n}\n\nfunc TestMultiSelectFiltering(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\tfiltering bool\n\t}{\n\t\t{\"Filtering off\", false},\n\t\t{\"Filtering on\", true},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tfield := NewMultiSelect[string]().Options(NewOptions(\"Foo\", \"Bar\", \"Baz\")...).Title(\"Which one?\").Filterable(tc.filtering)\n\t\t\tf := NewForm(NewGroup(field))\n\t\t\tf.Update(f.Init())\n\t\t\t// Filter for values starting with a 'B' only.\n\t\t\tf.Update(keypress('/'))\n\t\t\tf.Update(keypress('B'))\n\n\t\t\tview := viewModel(f)\n\t\t\t// When we're filtering, the list should change.\n\t\t\tif tc.filtering && strings.Contains(view, \"Foo\") {\n\t\t\t\tt.Log(pretty.Render(view))\n\t\t\t\tt.Error(\"Foo should not in filtered list.\")\n\t\t\t}\n\t\t\t// When we're not filtering, the list shouldn't change.\n\t\t\tif !tc.filtering && !strings.Contains(view, \"Foo\") {\n\t\t\t\tt.Log(pretty.Render(view))\n\t\t\t\tt.Error(\"Expected list to contain Foo.\")\n\t\t\t}\n\t\t})\n\t}\n\tt.Run(\"Remove filter option from help menu.\", func(t *testing.T) {\n\t\tfield := NewMultiSelect[string]().Options(NewOptions(\"Foo\", \"Bar\", \"Baz\")...).Title(\"Which one?\").Filterable(false)\n\t\tf := NewForm(NewGroup(field))\n\t\tf.Update(f.Init())\n\t\tview := viewModel(f)\n\t\tif strings.Contains(view, \"filter\") {\n\t\t\tt.Log(pretty.Render(view))\n\t\t\tt.Error(\"Expected list to hide filtering in help menu.\")\n\t\t}\n\t})\n}\n\nfunc TestSelectPageNavigation(t *testing.T) {\n\topts := NewOptions(\n\t\t\"Qux\",\n\t\t\"Quux\",\n\t\t\"Foo\",\n\t\t\"Bar\",\n\t\t\"Baz\",\n\t\t\"Corge\",\n\t\t\"Grault\",\n\t\t\"Garply\",\n\t\t\"Waldo\",\n\t\t\"Fred\",\n\t\t\"Plugh\",\n\t\t\"Xyzzy\",\n\t\t\"Thud\",\n\t\t\"Norf\",\n\t\t\"Blip\",\n\t\t\"Flob\",\n\t\t\"Zorp\",\n\t\t\"Smurf\",\n\t\t\"Bloop\",\n\t\t\"Ping\",\n\t)\n\n\treFirst := regexp.MustCompile(`>( •)? Qux`)\n\treLast := regexp.MustCompile(`>( •)? Ping`)\n\treHalfDown := regexp.MustCompile(`>( •)? Baz`)\n\n\tfor name, field := range map[string]Field{\n\t\t\"multiselect\": NewMultiSelect[string]().Options(opts...).Title(\"Choose\"),\n\t\t\"select\":      NewSelect[string]().Options(opts...).Title(\"Choose\"),\n\t} {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tf := NewForm(NewGroup(field)).WithHeight(10)\n\t\t\tf.Update(f.Init())\n\n\t\t\tview := viewModel(f)\n\t\t\tif !reFirst.MatchString(view) {\n\t\t\t\tt.Log(pretty.Render(view))\n\t\t\t\tt.Errorf(\"Wrong item selected, should have matched %q (first item)\", reFirst.String())\n\t\t\t}\n\n\t\t\tm := batchUpdate(f.Update(keypress('G')))\n\t\t\t// if name == \"multiselect\" {\n\t\t\t// \tmm := field.(*MultiSelect[string])\n\t\t\t// \tt.Logf(\"AQUI: height=%d offset=%d\", mm.viewport.Height(), mm.viewport.YOffset())\n\t\t\t// \tt.Log(\"LOOK AT THIS SHIT\", ansi.Strip(mm.viewport.View()))\n\t\t\t// }\n\t\t\tview = viewModel(m)\n\t\t\tif !reLast.MatchString(view) {\n\t\t\t\tt.Log(pretty.Render(view))\n\t\t\t\tt.Errorf(\"Wrong item selected, should have matched %q (last item)\", reLast.String())\n\t\t\t}\n\n\t\t\tm = batchUpdate(f.Update(keypress('g')))\n\t\t\tview = viewModel(m)\n\t\t\tif !reFirst.MatchString(view) {\n\t\t\t\tt.Log(pretty.Render(view))\n\t\t\t\tt.Errorf(\"Wrong item selected, should have matched %q (first item)\", reFirst.String())\n\t\t\t}\n\n\t\t\tm = batchUpdate(f.Update(tea.KeyPressMsg(tea.Key{Mod: tea.ModCtrl, Code: 'd'})))\n\t\t\tview = viewModel(m)\n\t\t\tif !reHalfDown.MatchString(view) {\n\t\t\t\tt.Log(pretty.Render(view))\n\t\t\t\tt.Errorf(\"Wrong item selected, should have matched %q (half down item)\", reHalfDown.String())\n\t\t\t}\n\n\t\t\t// sends multiple to verify it stays within boundaries\n\t\t\tfor range 10 {\n\t\t\t\tm = batchUpdate(f.Update(tea.KeyPressMsg(tea.Key{Mod: tea.ModCtrl, Code: 'u'})))\n\t\t\t}\n\t\t\tview = viewModel(m)\n\t\t\tif !reFirst.MatchString(view) {\n\t\t\t\tt.Log(pretty.Render(view))\n\t\t\t\tt.Errorf(\"Wrong item selected, should have matched %q (first item)\", reFirst.String())\n\t\t\t}\n\n\t\t\t// verify it stays within boundaries\n\t\t\tfor range 10 {\n\t\t\t\tm = batchUpdate(f.Update(tea.KeyPressMsg(tea.Key{Mod: tea.ModCtrl, Code: 'd'})))\n\t\t\t}\n\t\t\tview = viewModel(m)\n\t\t\tif !reLast.MatchString(view) {\n\t\t\t\tt.Log(pretty.Render(view))\n\t\t\t\tt.Errorf(\"Wrong item selected, should have matched %q (last item)\", reLast.String())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestFile(t *testing.T) {\n\tfield := NewFilePicker().Title(\"Which file?\")\n\tcmd := field.Init()\n\tfield.Update(cmd())\n\n\tview := viewModel(field)\n\n\tif !strings.Contains(view, \"No file selected\") {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Error(\"Expected file picker to show no file selected.\")\n\t}\n\n\tif !strings.Contains(view, \"Which file?\") {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Error(\"Expected file picker to show title.\")\n\t}\n}\n\nfunc TestHideGroup(t *testing.T) {\n\tf := NewForm(\n\t\tNewGroup(NewNote().Description(\"Foo\")).\n\t\t\tWithHide(true),\n\t\tNewGroup(NewNote().Description(\"Bar\")),\n\t\tNewGroup(NewNote().Description(\"Baz\")),\n\t\tNewGroup(NewNote().Description(\"Qux\")).\n\t\t\tWithHideFunc(func() bool { return false }).\n\t\t\tWithHide(true),\n\t)\n\n\tf = batchUpdate(f, f.NextGroup()).(*Form)\n\n\tif v := f.View(); !strings.Contains(v, \"Bar\") {\n\t\tt.Log(pretty.Render(v))\n\t\tt.Error(\"expected Bar to be visible\")\n\t}\n\n\t// should have no effect as previous group is hidden\n\tf.Update(prevGroup())\n\n\tif v := f.View(); !strings.Contains(v, \"Bar\") {\n\t\tt.Log(pretty.Render(v))\n\t\tt.Error(\"expected Bar to be visible\")\n\t}\n\n\tf.Update(nextGroup())\n\n\tif v := f.View(); !strings.Contains(v, \"Baz\") {\n\t\tt.Log(pretty.Render(v))\n\t\tt.Error(\"expected Baz to be visible\")\n\t}\n\n\tf.Update(nextGroup())\n\n\tif v := f.View(); strings.Contains(v, \"Qux\") {\n\t\tt.Log(pretty.Render(v))\n\t\tt.Error(\"expected Qux to be hidden\")\n\t}\n\n\tif v := f.State; v != StateCompleted {\n\t\tt.Error(\"should have been completed\")\n\t}\n}\n\nfunc TestHideGroupLastAndFirstGroupsNotHidden(t *testing.T) {\n\tf := NewForm(\n\t\tNewGroup(NewNote().Description(\"Bar\")),\n\t\tNewGroup(NewNote().Description(\"Foo\")).\n\t\t\tWithHide(true),\n\t\tNewGroup(NewNote().Description(\"Baz\")),\n\t)\n\n\tf = batchUpdate(f, f.Init()).(*Form)\n\n\tif v := ansi.Strip(f.View()); !strings.Contains(v, \"Bar\") {\n\t\tt.Log(pretty.Render(v))\n\t\tt.Error(\"expected Bar to not be hidden\")\n\t}\n\n\t// should have no effect as there isn't any\n\tf.Update(prevGroup())\n\n\tif v := f.View(); !strings.Contains(v, \"Bar\") {\n\t\tt.Log(pretty.Render(v))\n\t\tt.Error(\"expected Bar to not be hidden\")\n\t}\n\n\tf.Update(nextGroup())\n\n\tif v := ansi.Strip(f.View()); !strings.Contains(v, \"Baz\") {\n\t\tt.Log(pretty.Render(v))\n\t\tt.Error(\"expected Baz to not be hidden\")\n\t}\n\n\t// should submit the form\n\tf.Update(nextGroup())\n\tif v := f.State; v != StateCompleted {\n\t\tt.Error(\"should have been completed\")\n\t}\n}\n\nfunc TestPrevGroup(t *testing.T) {\n\tf := NewForm(\n\t\tNewGroup(NewNote().Description(\"Bar\")),\n\t\tNewGroup(NewNote().Description(\"Foo\")),\n\t\tNewGroup(NewNote().Description(\"Baz\")),\n\t)\n\n\tf = batchUpdate(f, f.Init()).(*Form)\n\tf.Update(nextGroup())\n\tf.Update(nextGroup())\n\tf.Update(prevGroup())\n\tf.Update(prevGroup())\n\n\tif v := ansi.Strip(f.View()); !strings.Contains(v, \"Bar\") {\n\t\tt.Log(pretty.Render(v))\n\t\tt.Error(\"expected Bar to not be hidden\")\n\t}\n}\n\nfunc TestNote(t *testing.T) {\n\tfield := NewNote().\n\t\tTitle(\"Taco\").\n\t\tDescription(\"How may we take your order?\").\n\t\tNext(true)\n\tf := NewForm(NewGroup(field))\n\tf.Update(f.Init())\n\n\tview := viewModel(f)\n\n\tif !strings.Contains(view, \"Taco\") {\n\t\tt.Log(view)\n\t\tt.Error(\"Expected field to contain Taco title.\")\n\t}\n\n\tif !strings.Contains(view, \"order?\") {\n\t\tt.Log(view)\n\t\tt.Error(\"Expected field to contain Taco description.\")\n\t}\n\n\tif !strings.Contains(view, \"Next\") {\n\t\tt.Log(view)\n\t\tt.Error(\"Expected field to contain next button\")\n\t}\n\n\tconst expect = 7\n\tif h := lipgloss.Height(ansi.Strip(view)); h != expect {\n\t\tt.Log(view)\n\t\tt.Errorf(\"Expected field to have height %d, got %d\", expect, h)\n\t}\n\n\tif !strings.Contains(view, \"enter submit\") {\n\t\tt.Log(view)\n\t\tt.Error(\"Expected field to contain help.\")\n\t}\n}\n\nfunc TestDynamicHelp(t *testing.T) {\n\tf := NewForm(\n\t\tNewGroup(\n\t\t\tNewInput().Title(\"Dynamic Help\"),\n\t\t\tNewInput().Title(\"Dynamic Help\"),\n\t\t\tNewInput().Title(\"Dynamic Help\"),\n\t\t),\n\t)\n\tf.Update(f.Init())\n\n\tview := viewModel(f)\n\n\tif !strings.Contains(view, \"Dynamic Help\") {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Fatal(\"Expected help to contain title.\")\n\t}\n\n\tif strings.Contains(view, \"shift+tab\") || strings.Contains(view, \"submit\") {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Error(\"Expected help not to contain shift+tab or submit.\")\n\t}\n}\n\nfunc TestSkip(t *testing.T) {\n\tf := NewForm(\n\t\tNewGroup(\n\t\t\tNewInput().Title(\"First\"),\n\t\t\tNewNote().Title(\"Skipped\"),\n\t\t\tNewNote().Title(\"Skipped\"),\n\t\t\tNewInput().Title(\"Second\"),\n\t\t),\n\t).WithWidth(25)\n\n\tf = batchUpdate(f, f.Init()).(*Form)\n\tview := viewModel(f)\n\n\tif !strings.Contains(view, \"┃ First\") {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Error(\"Expected first field to be focused\")\n\t}\n\n\t// next field should skip both of the notes and proceed to the last input.\n\tf.Update(NextField())\n\tview = viewModel(f)\n\n\tif strings.Contains(view, \"┃ First\") {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Error(\"Expected first field to be blurred\")\n\t}\n\n\tif !strings.Contains(view, \"┃ Second\") {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Error(\"Expected second field to be focused\")\n\t}\n\n\t// previous field should skip both of the notes and focus the first input.\n\tf.Update(PrevField())\n\tview = viewModel(f)\n\n\tif strings.Contains(view, \"┃ Second\") {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Error(\"Expected second field to be blurred\")\n\t}\n\n\tif !strings.Contains(view, \"┃ First\") {\n\t\tt.Log(pretty.Render(view))\n\t\tt.Error(\"Expected first field to be focused\")\n\t}\n}\n\nfunc TestTimeout(t *testing.T) {\n\t// This test requires a real program, so make sure it doesn't interfere with our test runner.\n\tf := formProgram()\n\n\t// Test that the form times out after 1ms and returns a timeout error.\n\terr := f.WithTimeout(100 * time.Millisecond).Run()\n\tif err == nil || !errors.Is(err, ErrTimeout) {\n\t\tt.Errorf(\"expected timeout error, got %v\", err)\n\t}\n}\n\nfunc TestAbort(t *testing.T) {\n\t// This test requires a real program, so make sure it doesn't interfere with our test runner.\n\tf := formProgram()\n\n\t// Test that the form aborts without throwing a timeout error when explicitly told to abort.\n\tctx, cancel := context.WithCancel(context.Background())\n\t// Since the context is cancelled, the program should exit immediately.\n\tcancel()\n\t// Tell the form to abort.\n\tf.Update(tea.KeyPressMsg(tea.Key{Mod: tea.ModCtrl, Code: 'c'}))\n\t// Run the program.\n\terr := f.RunWithContext(ctx)\n\tif err == nil || !errors.Is(err, ErrUserAborted) {\n\t\tt.Errorf(\"expected user aborted error, got %v\", err)\n\t}\n}\n\nconst (\n\ttitle       = \"A Title\"\n\tdescription = \"A Description\"\n)\n\nvar titleAndDescTests = map[string]struct {\n\tEmpty       interface{ View() string }\n\tEmptyHeight int\n\tTitle       interface{ View() string }\n\tDescription interface{ View() string }\n}{\n\t\"Group\": {\n\t\tNewGroup(NewInput()),\n\t\t1, // >\n\t\tNewGroup(NewInput()).Title(title),\n\t\tNewGroup(NewInput()).Description(description),\n\t},\n\t\"Confirm\": {\n\t\tNewConfirm(),\n\t\t1, // yes | no\n\t\tNewConfirm().Title(title),\n\t\tNewConfirm().Description(description),\n\t},\n\t\"FilePicker\": {\n\t\tNewFilePicker(),\n\t\t1, // \"no file selected\"\n\t\tNewFilePicker().Title(title),\n\t\tNewFilePicker().Description(description),\n\t},\n\t\"Input\": {\n\t\tNewInput(),\n\t\t1, // >\n\t\tNewInput().Title(title),\n\t\tNewInput().Description(description),\n\t},\n\t\"Note\": {\n\t\tNewNote(),\n\t\t1, // |\n\t\tNewNote().Title(title),\n\t\tNewNote().Description(description),\n\t},\n\t\"Text\": {\n\t\tNewText(),\n\t\t6, // textarea\n\t\tNewText().Title(title),\n\t\tNewText().Description(description),\n\t},\n\t\"Select\": {\n\t\tNewSelect[string](),\n\t\t1, // >\n\t\tNewSelect[string]().Title(title),\n\t\tNewSelect[string]().Description(description),\n\t},\n\t\"MultiSelect\": {\n\t\tNewMultiSelect[string](),\n\t\t1, // >\n\t\tNewMultiSelect[string]().Title(title),\n\t\tNewMultiSelect[string]().Description(description),\n\t},\n}\n\nfunc TestNoTitleOrDescription(t *testing.T) {\n\tfor name, tt := range titleAndDescTests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tview := tt.Empty.View()\n\t\t\tgot := lipgloss.Height(ansi.Strip(view))\n\t\t\twant := tt.EmptyHeight\n\t\t\tif got != want {\n\t\t\t\tt.Log(pretty.Render(view))\n\t\t\t\tt.Fatalf(\"got != want; height should be %d, got %d\", want, got)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestTitleRowRender(t *testing.T) {\n\tfor name, tt := range titleAndDescTests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tview := tt.Title.View()\n\t\t\tif !strings.Contains(view, title) {\n\t\t\t\tt.Log(pretty.Render(view))\n\t\t\t\tt.Error(\"Expected title to be visible\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDescriptionRowRender(t *testing.T) {\n\tfor name, tt := range titleAndDescTests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tview := tt.Description.View()\n\t\t\tif !strings.Contains(view, description) {\n\t\t\t\tt.Log(pretty.Render(view))\n\t\t\t\tt.Error(\"Expected description to be visible\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGetFocusedField(t *testing.T) {\n\tf := NewForm(\n\t\tNewGroup(\n\t\t\tNewInput().Title(\"First\").Key(\"First\"),\n\t\t\tNewInput().Title(\"Second\").Key(\"Second\"),\n\t\t\tNewInput().Title(\"Third\").Key(\"Third\"),\n\t\t),\n\t).WithWidth(25)\n\tf = batchUpdate(f, f.Init()).(*Form)\n\n\tf.NextField()\n\tfield := f.GetFocusedField()\n\n\tif field.GetKey() != \"Second\" {\n\t\tt.Error(\"Expected Second field to be focused but was '\" + field.GetKey() + \"'\")\n\t}\n}\n\n// formProgram returns a new Form with a nil input and output, so it can be used as a test program.\nfunc formProgram() *Form {\n\treturn NewForm(NewGroup(NewInput().Title(\"Foo\"))).\n\t\tWithInput(nil).\n\t\tWithOutput(io.Discard).\n\t\tWithAccessible(false)\n}\n\nfunc batchUpdate(m Model, cmd tea.Cmd) Model {\n\tif cmd == nil {\n\t\treturn m\n\t}\n\tmsg := cmd()\n\tm, cmd = m.Update(msg)\n\tif cmd == nil {\n\t\treturn m\n\t}\n\tmsg = cmd()\n\tm, _ = m.Update(msg)\n\treturn m\n}\n\nfunc codeKeypress(r rune) tea.KeyPressMsg {\n\treturn tea.KeyPressMsg(tea.Key{\n\t\tCode: r,\n\t})\n}\n\nfunc keypress(r rune) tea.KeyPressMsg {\n\treturn tea.KeyPressMsg(tea.Key{\n\t\tText:        string(r),\n\t\tCode:        r,\n\t\tShiftedCode: r,\n\t})\n}\n\nfunc typeText[T Model](m T, s string) T {\n\tvar tm Model = m\n\tfor _, r := range s {\n\t\ttm, _ = tm.Update(keypress(r))\n\t}\n\treturn tm.(T)\n}\n\nfunc TestAccessibleForm(t *testing.T) {\n\tvar out bytes.Buffer\n\n\tf := NewForm(\n\t\tNewGroup(\n\t\t\tNewInput().Title(\"Hello:\"),\n\t\t),\n\t).\n\t\tWithAccessible(true).\n\t\tWithOutput(&out).\n\t\tWithInput(strings.NewReader(\"carlos\\n\"))\n\n\tif err := f.Run(); err != nil {\n\t\tt.Error(err)\n\t}\n\n\tif !strings.Contains(out.String(), \"Hello:\") {\n\t\tt.Error(\"invalid output:\\n\", out.String())\n\t}\n}\n\nfunc TestAccessibleFields(t *testing.T) {\n\tfor name, test := range map[string]struct {\n\t\tField       Field\n\t\tFieldFn     func() Field\n\t\tInput       string\n\t\tCheckOutput func(tb testing.TB, output string)\n\t\tCheckValue  func(tb testing.TB, value any)\n\t}{\n\t\t\"input\": {\n\t\t\tField: NewInput(),\n\t\t\tInput: \"Hello\",\n\t\t\tCheckOutput: func(tb testing.TB, output string) {\n\t\t\t\ttb.Helper()\n\t\t\t\trequireContains(tb, output, \"Input:\")\n\t\t\t},\n\t\t\tCheckValue: func(tb testing.TB, value any) {\n\t\t\t\ttb.Helper()\n\t\t\t\trequireEqual(tb, \"Hello\", value.(string))\n\t\t\t},\n\t\t},\n\t\t\"input with charlimit\": {\n\t\t\tField: NewInput().CharLimit(2),\n\t\t\tInput: \"Hello\",\n\t\t\tCheckOutput: func(tb testing.TB, output string) {\n\t\t\t\ttb.Helper()\n\t\t\t\trequireContains(tb, output, \"Input cannot exceed 2 characters\")\n\t\t\t},\n\t\t},\n\t\t\"input with default\": {\n\t\t\tFieldFn: func() Field {\n\t\t\t\tv := \"hi\"\n\t\t\t\treturn NewInput().Value(&v)\n\t\t\t},\n\t\t\tInput: \"\\n\",\n\t\t\tCheckOutput: func(tb testing.TB, output string) {\n\t\t\t\ttb.Helper()\n\t\t\t\trequireContains(tb, output, \"Input:\")\n\t\t\t},\n\t\t\tCheckValue: func(tb testing.TB, value any) {\n\t\t\t\ttb.Helper()\n\t\t\t\trequireEqual(tb, \"hi\", value.(string))\n\t\t\t},\n\t\t},\n\t\t\"confirm\": {\n\t\t\tField: NewConfirm(),\n\t\t\tInput: \"Y\",\n\t\t\tCheckOutput: func(tb testing.TB, output string) {\n\t\t\t\ttb.Helper()\n\t\t\t\trequireContains(tb, output, \"Choose [y/N]\")\n\t\t\t},\n\t\t\tCheckValue: func(tb testing.TB, value any) {\n\t\t\t\ttb.Helper()\n\t\t\t\trequireEqual(tb, true, value.(bool))\n\t\t\t},\n\t\t},\n\t\t\"confirm with default\": {\n\t\t\tFieldFn: func() Field {\n\t\t\t\tv := true\n\t\t\t\treturn NewConfirm().Value(&v)\n\t\t\t},\n\t\t\tCheckOutput: func(tb testing.TB, output string) {\n\t\t\t\ttb.Helper()\n\t\t\t\trequireContains(tb, output, \"Choose [Y/n]\")\n\t\t\t},\n\t\t\tCheckValue: func(tb testing.TB, value any) {\n\t\t\t\ttb.Helper()\n\t\t\t\trequireEqual(tb, true, value.(bool))\n\t\t\t},\n\t\t},\n\t\t\"confirm with default choose\": {\n\t\t\tFieldFn: func() Field {\n\t\t\t\tv := true\n\t\t\t\treturn NewConfirm().Value(&v)\n\t\t\t},\n\t\t\tInput: \"n\",\n\t\t\tCheckOutput: func(tb testing.TB, output string) {\n\t\t\t\ttb.Helper()\n\t\t\t\trequireContains(tb, output, \"Y/n\")\n\t\t\t},\n\t\t\tCheckValue: func(tb testing.TB, value any) {\n\t\t\t\ttb.Helper()\n\t\t\t\trequireEqual(tb, false, value.(bool))\n\t\t\t},\n\t\t},\n\t\t\"filepicker\": {\n\t\t\tField: NewFilePicker(),\n\t\t\tInput: \"huh_test.go\",\n\t\t\tCheckOutput: func(tb testing.TB, output string) {\n\t\t\t\ttb.Helper()\n\t\t\t\trequireContains(tb, output, \"Choose a file:\")\n\t\t\t},\n\t\t\tCheckValue: func(tb testing.TB, value any) {\n\t\t\t\ttb.Helper()\n\t\t\t\trequireEqual(tb, \"huh_test.go\", value.(string))\n\t\t\t},\n\t\t},\n\t\t\"filepicker with default\": {\n\t\t\tFieldFn: func() Field {\n\t\t\t\tv := \"huh_test.go\"\n\t\t\t\treturn NewFilePicker().Value(&v)\n\t\t\t},\n\t\t\tInput: \"\\n\",\n\t\t\tCheckOutput: func(tb testing.TB, output string) {\n\t\t\t\ttb.Helper()\n\t\t\t\trequireContains(tb, output, \"Choose a file:\")\n\t\t\t},\n\t\t\tCheckValue: func(tb testing.TB, value any) {\n\t\t\t\ttb.Helper()\n\t\t\t\trequireEqual(tb, \"huh_test.go\", value.(string))\n\t\t\t},\n\t\t},\n\t\t\"multiselect\": {\n\t\t\tField: NewMultiSelect[string]().Options(NewOptions(\"a\", \"b\")...),\n\t\t\tInput: \"2\",\n\t\t\tCheckOutput: func(tb testing.TB, output string) {\n\t\t\t\ttb.Helper()\n\t\t\t\trequireContains(tb, output, \"2. ✓ b\")\n\t\t\t},\n\t\t\tCheckValue: func(tb testing.TB, value any) {\n\t\t\t\ttb.Helper()\n\t\t\t\tgot := value.([]string)\n\t\t\t\trequireEqual(tb, 1, len(got))\n\t\t\t\trequireEqual(tb, \"b\", got[0])\n\t\t\t},\n\t\t},\n\t\t\"multiselect default value\": {\n\t\t\tFieldFn: func() Field {\n\t\t\t\tv := []string{\"b\", \"c\"}\n\t\t\t\treturn NewMultiSelect[string]().Options(NewOptions(\"a\", \"b\", \"c\", \"d\")...).Value(&v)\n\t\t\t},\n\t\t\tInput: \"\\n\",\n\t\t\tCheckOutput: func(tb testing.TB, output string) {\n\t\t\t\ttb.Helper()\n\t\t\t\trequireContains(tb, output, \"2. ✓ b\")\n\t\t\t\trequireContains(tb, output, \"3. ✓ c\")\n\t\t\t},\n\t\t},\n\t\t\"select\": {\n\t\t\tField: NewSelect[string]().Options(NewOptions(\"a\", \"b\")...),\n\t\t\tInput: \"2\",\n\t\t\tCheckOutput: func(tb testing.TB, output string) {\n\t\t\t\ttb.Helper()\n\t\t\t\trequireContains(tb, output, \"Select:\")\n\t\t\t},\n\t\t\tCheckValue: func(tb testing.TB, value any) {\n\t\t\t\ttb.Helper()\n\t\t\t\trequireEqual(tb, \"b\", value.(string))\n\t\t\t},\n\t\t},\n\t\t\"select default value\": {\n\t\t\tFieldFn: func() Field {\n\t\t\t\tv := \"c\"\n\t\t\t\treturn NewSelect[string]().\n\t\t\t\t\tOptions(NewOptions(\"a\", \"b\", \"c\", \"d\")...).\n\t\t\t\t\tValue(&v)\n\t\t\t},\n\t\t\tInput: \"\\n\",\n\t\t\tCheckOutput: func(tb testing.TB, output string) {\n\t\t\t\ttb.Helper()\n\t\t\t\trequireContains(tb, output, \"Select:\")\n\t\t\t\trequireContains(tb, output, \"Enter a number between 1 and 4\")\n\t\t\t},\n\t\t\tCheckValue: func(tb testing.TB, value any) {\n\t\t\t\ttb.Helper()\n\t\t\t\trequireEqual(tb, \"c\", value.(string))\n\t\t\t},\n\t\t},\n\t\t\"select no input\": {\n\t\t\tField: NewSelect[string]().Options(NewOptions(\"a\", \"b\")...),\n\t\t\tInput: \"\\n2\\n\",\n\t\t\tCheckOutput: func(tb testing.TB, output string) {\n\t\t\t\ttb.Helper()\n\t\t\t\trequireContains(tb, output, \"Select:\")\n\t\t\t\trequireContains(tb, output, \"Enter a number between 1 and 2\")\n\t\t\t\trequireContains(tb, output, \"Invalid: must be a number between 1 and 2\")\n\t\t\t},\n\t\t\tCheckValue: func(tb testing.TB, value any) {\n\t\t\t\ttb.Helper()\n\t\t\t\trequireEqual(tb, \"b\", value.(string))\n\t\t\t},\n\t\t},\n\t\t\"select single option\": {\n\t\t\tField: NewSelect[string]().Options(NewOptions(\"a\")...),\n\t\t\tInput: \"\\n1\\n\",\n\t\t\tCheckOutput: func(tb testing.TB, output string) {\n\t\t\t\ttb.Helper()\n\t\t\t\trequireContains(tb, output, \"Select:\")\n\t\t\t\trequireContains(tb, output, \"There is only one option available; enter the number 1:\")\n\t\t\t\trequireContains(tb, output, \"Invalid: must be 1\")\n\t\t\t},\n\t\t\tCheckValue: func(tb testing.TB, value any) {\n\t\t\t\ttb.Helper()\n\t\t\t\trequireEqual(tb, \"a\", value.(string))\n\t\t\t},\n\t\t},\n\t\t\"note\": {\n\t\t\tField: NewNote().Title(\"Hi\").Description(\"there\"),\n\t\t\tCheckOutput: func(tb testing.TB, output string) {\n\t\t\t\ttb.Helper()\n\t\t\t\trequireContains(tb, output, \"Hi\")\n\t\t\t\trequireContains(tb, output, \"there\")\n\t\t\t},\n\t\t},\n\t\t\"text\": {\n\t\t\tField: NewText().Title(\"Text:\"),\n\t\t\tInput: \"hello world\",\n\t\t\tCheckOutput: func(tb testing.TB, output string) {\n\t\t\t\ttb.Helper()\n\t\t\t\trequireContains(tb, output, \"Text:\")\n\t\t\t},\n\t\t\tCheckValue: func(tb testing.TB, value any) {\n\t\t\t\ttb.Helper()\n\t\t\t\trequireEqual(tb, \"hello world\", value.(string))\n\t\t\t},\n\t\t},\n\t\t\"text with limit\": {\n\t\t\tField: NewText().CharLimit(2).Title(\"Text\"),\n\t\t\tInput: \"hello world\",\n\t\t\tCheckOutput: func(tb testing.TB, output string) {\n\t\t\t\ttb.Helper()\n\t\t\t\trequireContains(tb, output, \"Input cannot exceed 2 characters\")\n\t\t\t},\n\t\t},\n\t\t\"text default value\": {\n\t\t\tFieldFn: func() Field {\n\t\t\t\tv := \"test\"\n\t\t\t\treturn NewText().Title(\"Text:\").Value(&v)\n\t\t\t},\n\t\t\tInput: \"\\n\",\n\t\t\tCheckOutput: func(tb testing.TB, output string) {\n\t\t\t\ttb.Helper()\n\t\t\t\trequireContains(tb, output, \"Text:\")\n\t\t\t},\n\t\t\tCheckValue: func(tb testing.TB, value any) {\n\t\t\t\ttb.Helper()\n\t\t\t\trequireEqual(tb, \"test\", value.(string))\n\t\t\t},\n\t\t},\n\t} {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tfield := test.Field\n\t\t\tif test.FieldFn != nil {\n\t\t\t\tfield = test.FieldFn()\n\t\t\t}\n\n\t\t\tvar out bytes.Buffer\n\t\t\tif err := field.RunAccessible(\n\t\t\t\t&out,\n\t\t\t\tstrings.NewReader(test.Input),\n\t\t\t); err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t}\n\t\t\tif test.CheckOutput != nil {\n\t\t\t\ttest.CheckOutput(t, out.String())\n\t\t\t}\n\t\t\tif test.CheckValue != nil {\n\t\t\t\ttest.CheckValue(t, field.GetValue())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestInputPasswordAccessible(t *testing.T) {\n\tt.Run(\"not a tty\", func(t *testing.T) {\n\t\tvar out bytes.Buffer\n\t\tif err := NewInput().\n\t\t\tEchoMode(EchoModeNone).\n\t\t\tRunAccessible(&out, bytes.NewReader(nil)); err == nil {\n\t\t\tt.Error(\"expected it to error\")\n\t\t}\n\t\tif err := NewInput().\n\t\t\tEchoMode(EchoModePassword).\n\t\t\tRunAccessible(&out, bytes.NewReader(nil)); err == nil {\n\t\t\tt.Error(\"expected it to error\")\n\t\t}\n\t})\n\n\tt.Run(\"is a tty\", func(t *testing.T) {\n\t\tvar out bytes.Buffer\n\t\tpty, err := xpty.NewPty(50, 30)\n\t\tif err != nil {\n\t\t\tt.Skipf(\"could not open pty: %v\", err)\n\t\t}\n\t\tupty, ok := pty.(*xpty.UnixPty)\n\t\tif !ok {\n\t\t\tt.Skipf(\"test only works on unix\")\n\t\t}\n\n\t\tinput := NewInput().EchoMode(EchoModePassword)\n\n\t\terrs := make(chan error, 1)\n\t\tgo func() {\n\t\t\terrs <- input.RunAccessible(&out, upty.Slave())\n\t\t}()\n\n\t\t_, _ = upty.Master().Write([]byte(\"a password\\n\"))\n\n\t\tif err := <-errs; err != nil {\n\t\t\tt.Errorf(\"expected no error, got %v\", err)\n\t\t}\n\n\t\tt.Logf(\"%q\", out.String())\n\t\trequireContains(t, out.String(), \"Password:\")\n\t\trequireEqual(t, \"a password\", input.GetValue().(string))\n\t})\n}\n\nfunc requireEqual[T comparable](tb testing.TB, a, b T) {\n\ttb.Helper()\n\tif a != b {\n\t\ttb.Fatalf(\"expected %v to be equal to %v\", a, b)\n\t}\n}\n\nfunc requireContains(tb testing.TB, s, subtr string) {\n\ttb.Helper()\n\tif !strings.Contains(s, subtr) {\n\t\ttb.Fatalf(\"%q does not contain %q\", s, subtr)\n\t}\n}\n\nfunc viewModel(m Model) string { return ansi.Strip(m.View()) }\n"
  },
  {
    "path": "internal/accessibility/accessibility.go",
    "content": "// Package accessibility provides accessible functions to capture user input.\npackage accessibility\n\nimport (\n\t\"bufio\"\n\t\"cmp\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/charmbracelet/x/term\"\n)\n\nfunc atoi(s string) (int, error) {\n\tif strings.TrimSpace(s) == \"\" {\n\t\treturn -1, nil\n\t}\n\treturn strconv.Atoi(s) //nolint:wrapcheck\n}\n\n// PromptInt prompts a user for an integer between a certain range.\n//\n// Given invalid input (non-integers, integers outside of the range), the user\n// will continue to be reprompted until a valid input is given, ensuring that\n// the return value is always valid.\nfunc PromptInt(\n\tout io.Writer,\n\tin io.Reader,\n\tprompt string,\n\tlow, high int,\n\tdefaultValue *int,\n) int {\n\tvar choice int\n\n\tvalidInt := func(s string) error {\n\t\tif strings.TrimSpace(s) == \"\" && defaultValue != nil {\n\t\t\treturn nil\n\t\t}\n\t\ti, err := atoi(s)\n\t\tif err != nil || i < low || i > high {\n\t\t\tif low == high {\n\t\t\t\treturn fmt.Errorf(\"Invalid: must be %d\", low) //nolint:staticcheck\n\t\t\t}\n\t\t\treturn fmt.Errorf(\"Invalid: must be a number between %d and %d\", low, high) //nolint:staticcheck\n\t\t}\n\t\treturn nil\n\t}\n\n\tinput := PromptString(\n\t\tout,\n\t\tin,\n\t\tprompt,\n\t\tptrToStr(defaultValue, strconv.Itoa),\n\t\tvalidInt,\n\t)\n\tchoice, _ = strconv.Atoi(input)\n\treturn choice\n}\n\nfunc parseBool(s string) (bool, error) {\n\ts = strings.ToLower(s)\n\n\tif slices.Contains([]string{\"y\", \"yes\"}, s) {\n\t\treturn true, nil\n\t}\n\n\t// As a special case, we default to \"\" to no since the usage of this\n\t// function suggests N is the default.\n\tif slices.Contains([]string{\"n\", \"no\"}, s) {\n\t\treturn false, nil\n\t}\n\n\treturn false, errors.New(\"invalid input. please try again\")\n}\n\n// PromptBool prompts a user for a boolean value.\n//\n// Given invalid input (non-boolean), the user will continue to be reprompted\n// until a valid input is given, ensuring that the return value is always valid.\nfunc PromptBool(\n\tout io.Writer,\n\tin io.Reader,\n\tprompt string,\n\tdefaultValue bool,\n) bool {\n\tvalidBool := func(s string) error {\n\t\tif strings.TrimSpace(s) == \"\" {\n\t\t\treturn nil\n\t\t}\n\t\t_, err := parseBool(s)\n\t\treturn err\n\t}\n\n\tinput := PromptString(\n\t\tout, in, prompt,\n\t\tboolToStr(defaultValue),\n\t\tvalidBool,\n\t)\n\tb, _ := parseBool(input)\n\treturn b\n}\n\n// PromptPassword allows to prompt for a password.\n// In must be the fd of a tty.\nfunc PromptPassword(\n\tout io.Writer,\n\tin uintptr,\n\tprompt string,\n\tvalidator func(input string) error,\n) (string, error) {\n\tfor {\n\t\t_, _ = fmt.Fprint(out, prompt)\n\t\tpwd, err := term.ReadPassword(in)\n\t\tif err != nil {\n\t\t\treturn \"\", err //nolint:wrapcheck\n\t\t}\n\t\t_, _ = fmt.Fprintln(out)\n\t\tif err := validator(string(pwd)); err != nil {\n\t\t\t_, _ = fmt.Fprintln(out, err)\n\t\t\tcontinue\n\t\t}\n\t\treturn string(pwd), nil\n\t}\n}\n\n// PromptString prompts a user for a string value and validates it against a\n// validator function. It re-prompts the user until a valid input is given.\nfunc PromptString(\n\tout io.Writer,\n\tin io.Reader,\n\tprompt string,\n\tdefaultValue string,\n\tvalidator func(input string) error,\n) string {\n\tscanner := bufio.NewScanner(in)\n\n\tvar (\n\t\tvalid bool\n\t\tinput string\n\t)\n\n\tfor !valid {\n\t\t_, _ = fmt.Fprint(out, prompt)\n\t\tif !scanner.Scan() {\n\t\t\t// no way to bubble up errors or signal cancellation\n\t\t\t// but the program is probably not continuing if\n\t\t\t// stdin sent EOF\n\t\t\t_, _ = fmt.Fprintln(out)\n\t\t\tbreak\n\t\t}\n\t\tinput = scanner.Text()\n\n\t\tif err := validator(input); err != nil {\n\t\t\t_, _ = fmt.Fprintln(out, err)\n\t\t\tcontinue\n\t\t}\n\n\t\tbreak\n\t}\n\n\treturn cmp.Or(strings.TrimSpace(input), defaultValue)\n}\n\nfunc ptrToStr[T any](t *T, fn func(t T) string) string {\n\tif t == nil {\n\t\treturn \"\"\n\t}\n\treturn fn(*t)\n}\n\nfunc boolToStr(b bool) string {\n\tif b {\n\t\treturn \"y\"\n\t}\n\treturn \"N\"\n}\n"
  },
  {
    "path": "internal/compat/model.go",
    "content": "// Package compat provides common types used across the application.\npackage compat\n\nimport tea \"charm.land/bubbletea/v2\"\n\n// Model is a bubbletea v1 [tea.Model].\ntype Model interface {\n\tInit() tea.Cmd\n\tUpdate(msg tea.Msg) (Model, tea.Cmd)\n\tView() string\n}\n\n// ViewHook is a function that modifies a [tea.View].\ntype ViewHook = func(tea.View) tea.View\n\n// ViewModel wraps a [Model] and [ViewHook].\ntype ViewModel struct {\n\tModel\n\tViewHook ViewHook\n}\n\n// Update implements [tea.Model].\nfunc (w ViewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {\n\tm, cmd := w.Model.Update(msg)\n\treturn ViewModel{\n\t\tModel:    m,\n\t\tViewHook: w.ViewHook,\n\t}, cmd\n}\n\n// View implements [tea.Model].\nfunc (w ViewModel) View() tea.View {\n\tvar view tea.View\n\tif w.ViewHook != nil {\n\t\tview = w.ViewHook(view)\n\t}\n\tview.SetContent(w.Model.View())\n\treturn view\n}\n"
  },
  {
    "path": "internal/selector/selector.go",
    "content": "// Package selector provides a helper type for selecting items.\npackage selector\n\n// Selector is a helper type for selecting items.\ntype Selector[T any] struct {\n\titems []T\n\tindex int\n}\n\n// NewSelector creates a new item selector.\nfunc NewSelector[T any](items []T) *Selector[T] {\n\treturn &Selector[T]{\n\t\titems: items,\n\t}\n}\n\n// Append adds an item to the selector.\nfunc (s *Selector[T]) Append(item T) {\n\ts.items = append(s.items, item)\n}\n\n// Next moves the selector to the next item.\nfunc (s *Selector[T]) Next() {\n\tif s.index < len(s.items)-1 {\n\t\ts.index++\n\t}\n}\n\n// Prev moves the selector to the previous item.\nfunc (s *Selector[T]) Prev() {\n\tif s.index > 0 {\n\t\ts.index--\n\t}\n}\n\n// OnFirst returns true if the selector is on the first item.\nfunc (s *Selector[T]) OnFirst() bool {\n\treturn s.index == 0\n}\n\n// OnLast returns true if the selector is on the last item.\nfunc (s *Selector[T]) OnLast() bool {\n\treturn s.index == len(s.items)-1\n}\n\n// Selected returns the index of the current selected item.\nfunc (s *Selector[T]) Selected() T {\n\treturn s.items[s.index]\n}\n\n// Index returns the index of the current selected item.\nfunc (s *Selector[T]) Index() int {\n\treturn s.index\n}\n\n// Total returns the total number of items.\nfunc (s *Selector[T]) Total() int {\n\treturn len(s.items)\n}\n\n// SetIndex sets the selected item.\nfunc (s *Selector[T]) SetIndex(i int) {\n\tif i < 0 || i >= len(s.items) {\n\t\treturn\n\t}\n\ts.index = i\n}\n\n// Get returns the item at the given index.\nfunc (s *Selector[T]) Get(i int) T {\n\treturn s.items[i]\n}\n\n// Set sets the item at the given index.\nfunc (s *Selector[T]) Set(i int, item T) {\n\ts.items[i] = item\n}\n\n// Range iterates over the items.\n// The callback function should return true to continue the iteration.\nfunc (s *Selector[T]) Range(f func(i int, item T) bool) {\n\tfor i, item := range s.items {\n\t\tif !f(i, item) {\n\t\t\tbreak\n\t\t}\n\t}\n}\n\n// ReverseRange iterates over the items in reverse.\n// The callback function should return true to continue the iteration.\nfunc (s *Selector[T]) ReverseRange(f func(i int, item T) bool) {\n\tfor i := len(s.items) - 1; i >= 0; i-- {\n\t\tif !f(i, s.items[i]) {\n\t\t\tbreak\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "keymap.go",
    "content": "package huh\n\nimport \"charm.land/bubbles/v2/key\"\n\n// KeyMap is the keybindings to navigate the form.\ntype KeyMap struct {\n\tQuit key.Binding\n\n\tConfirm     ConfirmKeyMap\n\tFilePicker  FilePickerKeyMap\n\tInput       InputKeyMap\n\tMultiSelect MultiSelectKeyMap\n\tNote        NoteKeyMap\n\tSelect      SelectKeyMap\n\tText        TextKeyMap\n}\n\n// InputKeyMap is the keybindings for input fields.\ntype InputKeyMap struct {\n\tAcceptSuggestion key.Binding\n\tNext             key.Binding\n\tPrev             key.Binding\n\tSubmit           key.Binding\n}\n\n// TextKeyMap is the keybindings for text fields.\ntype TextKeyMap struct {\n\tNext    key.Binding\n\tPrev    key.Binding\n\tNewLine key.Binding\n\tEditor  key.Binding\n\tSubmit  key.Binding\n}\n\n// SelectKeyMap is the keybindings for select fields.\ntype SelectKeyMap struct {\n\tNext         key.Binding\n\tPrev         key.Binding\n\tUp           key.Binding\n\tDown         key.Binding\n\tHalfPageUp   key.Binding\n\tHalfPageDown key.Binding\n\tGotoTop      key.Binding\n\tGotoBottom   key.Binding\n\tLeft         key.Binding\n\tRight        key.Binding\n\tFilter       key.Binding\n\tSetFilter    key.Binding\n\tClearFilter  key.Binding\n\tSubmit       key.Binding\n}\n\n// MultiSelectKeyMap is the keybindings for multi-select fields.\ntype MultiSelectKeyMap struct {\n\tNext         key.Binding\n\tPrev         key.Binding\n\tUp           key.Binding\n\tDown         key.Binding\n\tHalfPageUp   key.Binding\n\tHalfPageDown key.Binding\n\tGotoTop      key.Binding\n\tGotoBottom   key.Binding\n\tToggle       key.Binding\n\tFilter       key.Binding\n\tSetFilter    key.Binding\n\tClearFilter  key.Binding\n\tSubmit       key.Binding\n\tSelectAll    key.Binding\n\tSelectNone   key.Binding\n}\n\n// FilePickerKeyMap is the keybindings for filepicker fields.\ntype FilePickerKeyMap struct {\n\tOpen       key.Binding\n\tClose      key.Binding\n\tGotoTop    key.Binding\n\tGotoBottom key.Binding\n\tPageUp     key.Binding\n\tPageDown   key.Binding\n\tBack       key.Binding\n\tSelect     key.Binding\n\tUp         key.Binding\n\tDown       key.Binding\n\tPrev       key.Binding\n\tNext       key.Binding\n\tSubmit     key.Binding\n}\n\n// NoteKeyMap is the keybindings for note fields.\ntype NoteKeyMap struct {\n\tNext   key.Binding\n\tPrev   key.Binding\n\tSubmit key.Binding\n}\n\n// ConfirmKeyMap is the keybindings for confirm fields.\ntype ConfirmKeyMap struct {\n\tNext   key.Binding\n\tPrev   key.Binding\n\tToggle key.Binding\n\tSubmit key.Binding\n\tAccept key.Binding\n\tReject key.Binding\n}\n\n// NewDefaultKeyMap returns a new default keymap.\nfunc NewDefaultKeyMap() *KeyMap {\n\treturn &KeyMap{\n\t\tQuit: key.NewBinding(key.WithKeys(\"ctrl+c\")),\n\t\tInput: InputKeyMap{\n\t\t\tAcceptSuggestion: key.NewBinding(key.WithKeys(\"ctrl+e\"), key.WithHelp(\"ctrl+e\", \"complete\")),\n\t\t\tPrev:             key.NewBinding(key.WithKeys(\"shift+tab\"), key.WithHelp(\"shift+tab\", \"back\")),\n\t\t\tNext:             key.NewBinding(key.WithKeys(\"enter\", \"tab\"), key.WithHelp(\"enter\", \"next\")),\n\t\t\tSubmit:           key.NewBinding(key.WithKeys(\"enter\"), key.WithHelp(\"enter\", \"submit\")),\n\t\t},\n\t\tFilePicker: FilePickerKeyMap{\n\t\t\tGotoTop:    key.NewBinding(key.WithKeys(\"g\"), key.WithHelp(\"g\", \"first\"), key.WithDisabled()),\n\t\t\tGotoBottom: key.NewBinding(key.WithKeys(\"G\"), key.WithHelp(\"G\", \"last\"), key.WithDisabled()),\n\t\t\tPageUp:     key.NewBinding(key.WithKeys(\"K\", \"pgup\"), key.WithHelp(\"pgup\", \"page up\"), key.WithDisabled()),\n\t\t\tPageDown:   key.NewBinding(key.WithKeys(\"J\", \"pgdown\"), key.WithHelp(\"pgdown\", \"page down\"), key.WithDisabled()),\n\t\t\tBack:       key.NewBinding(key.WithKeys(\"h\", \"backspace\", \"left\", \"esc\"), key.WithHelp(\"h\", \"back\"), key.WithDisabled()),\n\t\t\tSelect:     key.NewBinding(key.WithKeys(\"enter\"), key.WithHelp(\"enter\", \"select\"), key.WithDisabled()),\n\t\t\tUp:         key.NewBinding(key.WithKeys(\"up\", \"k\", \"ctrl+k\", \"ctrl+p\"), key.WithHelp(\"↑\", \"up\"), key.WithDisabled()),\n\t\t\tDown:       key.NewBinding(key.WithKeys(\"down\", \"j\", \"ctrl+j\", \"ctrl+n\"), key.WithHelp(\"↓\", \"down\"), key.WithDisabled()),\n\n\t\t\tOpen:   key.NewBinding(key.WithKeys(\"l\", \"right\", \"enter\"), key.WithHelp(\"enter\", \"open\")),\n\t\t\tClose:  key.NewBinding(key.WithKeys(\"esc\"), key.WithHelp(\"esc\", \"close\"), key.WithDisabled()),\n\t\t\tPrev:   key.NewBinding(key.WithKeys(\"shift+tab\"), key.WithHelp(\"shift+tab\", \"back\")),\n\t\t\tNext:   key.NewBinding(key.WithKeys(\"tab\"), key.WithHelp(\"tab\", \"next\")),\n\t\t\tSubmit: key.NewBinding(key.WithKeys(\"enter\"), key.WithHelp(\"enter\", \"submit\")),\n\t\t},\n\t\tText: TextKeyMap{\n\t\t\tPrev:    key.NewBinding(key.WithKeys(\"shift+tab\"), key.WithHelp(\"shift+tab\", \"back\")),\n\t\t\tNext:    key.NewBinding(key.WithKeys(\"tab\", \"enter\"), key.WithHelp(\"enter\", \"next\")),\n\t\t\tSubmit:  key.NewBinding(key.WithKeys(\"enter\"), key.WithHelp(\"enter\", \"submit\")),\n\t\t\tNewLine: key.NewBinding(key.WithKeys(\"alt+enter\", \"ctrl+j\"), key.WithHelp(\"alt+enter / ctrl+j\", \"new line\")),\n\t\t\tEditor:  key.NewBinding(key.WithKeys(\"ctrl+e\"), key.WithHelp(\"ctrl+e\", \"open editor\")),\n\t\t},\n\t\tSelect: SelectKeyMap{\n\t\t\tPrev:         key.NewBinding(key.WithKeys(\"shift+tab\"), key.WithHelp(\"shift+tab\", \"back\")),\n\t\t\tNext:         key.NewBinding(key.WithKeys(\"enter\", \"tab\"), key.WithHelp(\"enter\", \"select\")),\n\t\t\tSubmit:       key.NewBinding(key.WithKeys(\"enter\"), key.WithHelp(\"enter\", \"submit\")),\n\t\t\tUp:           key.NewBinding(key.WithKeys(\"up\", \"k\", \"ctrl+k\", \"ctrl+p\"), key.WithHelp(\"↑\", \"up\")),\n\t\t\tDown:         key.NewBinding(key.WithKeys(\"down\", \"j\", \"ctrl+j\", \"ctrl+n\"), key.WithHelp(\"↓\", \"down\")),\n\t\t\tLeft:         key.NewBinding(key.WithKeys(\"h\", \"left\"), key.WithHelp(\"←\", \"left\"), key.WithDisabled()),\n\t\t\tRight:        key.NewBinding(key.WithKeys(\"l\", \"right\"), key.WithHelp(\"→\", \"right\"), key.WithDisabled()),\n\t\t\tFilter:       key.NewBinding(key.WithKeys(\"/\"), key.WithHelp(\"/\", \"filter\")),\n\t\t\tSetFilter:    key.NewBinding(key.WithKeys(\"esc\"), key.WithHelp(\"esc\", \"set filter\"), key.WithDisabled()),\n\t\t\tClearFilter:  key.NewBinding(key.WithKeys(\"esc\"), key.WithHelp(\"esc\", \"clear filter\"), key.WithDisabled()),\n\t\t\tHalfPageUp:   key.NewBinding(key.WithKeys(\"ctrl+u\"), key.WithHelp(\"ctrl+u\", \"½ page up\")),\n\t\t\tHalfPageDown: key.NewBinding(key.WithKeys(\"ctrl+d\"), key.WithHelp(\"ctrl+d\", \"½ page down\")),\n\t\t\tGotoTop:      key.NewBinding(key.WithKeys(\"home\", \"g\"), key.WithHelp(\"g/home\", \"go to start\")),\n\t\t\tGotoBottom:   key.NewBinding(key.WithKeys(\"end\", \"G\"), key.WithHelp(\"G/end\", \"go to end\")),\n\t\t},\n\t\tMultiSelect: MultiSelectKeyMap{\n\t\t\tPrev:         key.NewBinding(key.WithKeys(\"shift+tab\"), key.WithHelp(\"shift+tab\", \"back\")),\n\t\t\tNext:         key.NewBinding(key.WithKeys(\"enter\", \"tab\"), key.WithHelp(\"enter\", \"confirm\")),\n\t\t\tSubmit:       key.NewBinding(key.WithKeys(\"enter\"), key.WithHelp(\"enter\", \"submit\")),\n\t\t\tToggle:       key.NewBinding(key.WithKeys(\"space\", \"x\"), key.WithHelp(\"x\", \"toggle\")),\n\t\t\tUp:           key.NewBinding(key.WithKeys(\"up\", \"k\", \"ctrl+p\"), key.WithHelp(\"↑\", \"up\")),\n\t\t\tDown:         key.NewBinding(key.WithKeys(\"down\", \"j\", \"ctrl+n\"), key.WithHelp(\"↓\", \"down\")),\n\t\t\tFilter:       key.NewBinding(key.WithKeys(\"/\"), key.WithHelp(\"/\", \"filter\")),\n\t\t\tSetFilter:    key.NewBinding(key.WithKeys(\"enter\", \"esc\"), key.WithHelp(\"esc\", \"set filter\"), key.WithDisabled()),\n\t\t\tClearFilter:  key.NewBinding(key.WithKeys(\"esc\"), key.WithHelp(\"esc\", \"clear filter\"), key.WithDisabled()),\n\t\t\tHalfPageUp:   key.NewBinding(key.WithKeys(\"ctrl+u\"), key.WithHelp(\"ctrl+u\", \"½ page up\")),\n\t\t\tHalfPageDown: key.NewBinding(key.WithKeys(\"ctrl+d\"), key.WithHelp(\"ctrl+d\", \"½ page down\")),\n\t\t\tGotoTop:      key.NewBinding(key.WithKeys(\"home\", \"g\"), key.WithHelp(\"g/home\", \"go to start\")),\n\t\t\tGotoBottom:   key.NewBinding(key.WithKeys(\"end\", \"G\"), key.WithHelp(\"G/end\", \"go to end\")),\n\t\t\tSelectAll:    key.NewBinding(key.WithKeys(\"ctrl+a\"), key.WithHelp(\"ctrl+a\", \"select all\")),\n\t\t\tSelectNone:   key.NewBinding(key.WithKeys(\"ctrl+a\"), key.WithHelp(\"ctrl+a\", \"select none\"), key.WithDisabled()),\n\t\t},\n\t\tNote: NoteKeyMap{\n\t\t\tPrev:   key.NewBinding(key.WithKeys(\"shift+tab\"), key.WithHelp(\"shift+tab\", \"back\")),\n\t\t\tNext:   key.NewBinding(key.WithKeys(\"enter\", \"tab\"), key.WithHelp(\"enter\", \"next\")),\n\t\t\tSubmit: key.NewBinding(key.WithKeys(\"enter\"), key.WithHelp(\"enter\", \"submit\")),\n\t\t},\n\t\tConfirm: ConfirmKeyMap{\n\t\t\tPrev:   key.NewBinding(key.WithKeys(\"shift+tab\"), key.WithHelp(\"shift+tab\", \"back\")),\n\t\t\tNext:   key.NewBinding(key.WithKeys(\"enter\", \"tab\"), key.WithHelp(\"enter\", \"next\")),\n\t\t\tSubmit: key.NewBinding(key.WithKeys(\"enter\"), key.WithHelp(\"enter\", \"submit\")),\n\t\t\tToggle: key.NewBinding(key.WithKeys(\"h\", \"l\", \"right\", \"left\"), key.WithHelp(\"←/→\", \"toggle\")),\n\t\t\tAccept: key.NewBinding(key.WithKeys(\"y\", \"Y\"), key.WithHelp(\"y\", \"Yes\")),\n\t\t\tReject: key.NewBinding(key.WithKeys(\"n\", \"N\"), key.WithHelp(\"n\", \"No\")),\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "layout.go",
    "content": "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 form.\ntype Layout interface {\n\tView(f *Form) string\n\tGroupWidth(f *Form, g *Group, w int) int\n}\n\n// LayoutDefault is the default layout shows a single group at a time.\nvar LayoutDefault Layout = &layoutDefault{}\n\n// LayoutStack is a layout stacks all groups on top of each other.\nvar LayoutStack Layout = &layoutStack{}\n\n// LayoutColumns layout distributes groups in even columns.\nfunc LayoutColumns(columns int) Layout {\n\treturn &layoutColumns{columns: columns}\n}\n\n// LayoutGrid layout distributes groups in a grid.\nfunc LayoutGrid(rows int, columns int) Layout {\n\treturn &layoutGrid{rows: rows, columns: columns}\n}\n\ntype layoutDefault struct{}\n\nfunc (l *layoutDefault) View(f *Form) string {\n\treturn f.selector.Selected().View()\n}\n\nfunc (l *layoutDefault) GroupWidth(_ *Form, _ *Group, w int) int {\n\treturn w\n}\n\ntype layoutColumns struct {\n\tcolumns int\n}\n\nfunc (l *layoutColumns) visibleGroups(f *Form) []*Group {\n\tsegmentIndex := f.selector.Index() / l.columns\n\tstart := segmentIndex * l.columns\n\tend := start + l.columns\n\n\ttotal := f.selector.Total()\n\tif end > total {\n\t\tend = total\n\t}\n\n\tvar groups []*Group\n\tf.selector.Range(func(i int, group *Group) bool {\n\t\tif i >= start && i < end {\n\t\t\tgroups = append(groups, group)\n\t\t\treturn true\n\t\t}\n\t\treturn true\n\t})\n\n\treturn groups\n}\n\nfunc (l *layoutColumns) View(f *Form) string {\n\tgroups := l.visibleGroups(f)\n\tif len(groups) == 0 {\n\t\treturn \"\"\n\t}\n\n\tcolumns := make([]string, 0, len(groups))\n\tfor _, group := range groups {\n\t\tcolumns = append(columns, group.Content())\n\t}\n\n\theader := f.selector.Selected().Header()\n\tfooter := f.selector.Selected().Footer()\n\n\treturn strings.Join([]string{\n\t\theader,\n\t\tlipgloss.JoinHorizontal(lipgloss.Left, columns...),\n\t\tfooter,\n\t}, \"\\n\")\n}\n\nfunc (l *layoutColumns) GroupWidth(_ *Form, _ *Group, w int) int {\n\treturn w / l.columns\n}\n\ntype layoutStack struct{}\n\nfunc (l *layoutStack) View(f *Form) string {\n\tvar columns []string\n\tf.selector.Range(func(_ int, group *Group) bool {\n\t\tcolumns = append(columns, group.Content(), \"\")\n\t\treturn true\n\t})\n\n\tif footer := f.selector.Selected().Footer(); footer != \"\" {\n\t\tcolumns = append(columns, footer)\n\t}\n\treturn strings.Join(columns, \"\\n\")\n}\n\nfunc (l *layoutStack) GroupWidth(_ *Form, _ *Group, w int) int {\n\treturn w\n}\n\ntype layoutGrid struct {\n\trows, columns int\n}\n\nfunc (l *layoutGrid) visibleGroups(f *Form) [][]*Group {\n\ttotal := l.rows * l.columns\n\tsegmentIndex := f.selector.Index() / total\n\tstart := segmentIndex * total\n\tend := start + total\n\n\tif glen := f.selector.Total(); end > glen {\n\t\tend = glen\n\t}\n\n\tvar visible []*Group\n\tf.selector.Range(func(i int, group *Group) bool {\n\t\tif i >= start && i < end {\n\t\t\tvisible = append(visible, group)\n\t\t\treturn true\n\t\t}\n\t\treturn true\n\t})\n\tgrid := make([][]*Group, l.rows)\n\tfor i := 0; i < l.rows; i++ {\n\t\tstartRow := i * l.columns\n\t\tendRow := startRow + l.columns\n\t\tif startRow >= len(visible) {\n\t\t\tbreak\n\t\t}\n\t\tif endRow > len(visible) {\n\t\t\tendRow = len(visible)\n\t\t}\n\t\tgrid[i] = visible[startRow:endRow]\n\t}\n\treturn grid\n}\n\nfunc (l *layoutGrid) View(f *Form) string {\n\tgrid := l.visibleGroups(f)\n\tif len(grid) == 0 {\n\t\treturn \"\"\n\t}\n\n\trows := make([]string, 0, len(grid))\n\tfor _, row := range grid {\n\t\tvar columns []string\n\t\tfor _, group := range row {\n\t\t\tcolumns = append(columns, group.Content())\n\t\t}\n\t\trows = append(rows, lipgloss.JoinHorizontal(lipgloss.Left, columns...), \"\")\n\t}\n\tfooter := f.selector.Selected().Footer()\n\n\treturn strings.Join(append(rows, footer), \"\\n\")\n}\n\nfunc (l *layoutGrid) GroupWidth(_ *Form, _ *Group, w int) int {\n\treturn w / l.columns\n}\n"
  },
  {
    "path": "option.go",
    "content": "package huh\n\nimport \"fmt\"\n\n// Option is an option for select fields.\ntype Option[T comparable] struct {\n\tKey      string\n\tValue    T\n\tselected bool\n}\n\n// NewOptions returns new options from a list of values.\nfunc NewOptions[T comparable](values ...T) []Option[T] {\n\toptions := make([]Option[T], len(values))\n\tfor i, o := range values {\n\t\toptions[i] = Option[T]{\n\t\t\tKey:   fmt.Sprint(o),\n\t\t\tValue: o,\n\t\t}\n\t}\n\treturn options\n}\n\n// NewOption returns a new select option.\nfunc NewOption[T comparable](key string, value T) Option[T] {\n\treturn Option[T]{Key: key, Value: value}\n}\n\n// Selected sets whether the option is currently selected.\nfunc (o Option[T]) Selected(selected bool) Option[T] {\n\to.selected = selected\n\treturn o\n}\n\n// String returns the key of the option.\nfunc (o Option[T]) String() string {\n\treturn o.Key\n}\n"
  },
  {
    "path": "run.go",
    "content": "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 := NewGroup(field)\n\tform := NewForm(group).WithShowHelp(false)\n\treturn form.Run()\n}\n"
  },
  {
    "path": "spinner/spinner.go",
    "content": "// 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\"charm.land/bubbles/v2/spinner\"\n\ttea \"charm.land/bubbletea/v2\"\n\t\"charm.land/huh/v2/internal/compat\"\n\t\"charm.land/lipgloss/v2\"\n\t\"github.com/charmbracelet/x/ansi\"\n\t\"github.com/charmbracelet/x/term\"\n)\n\n// Model is an alias to [compat.Model].\ntype Model = compat.Model\n\n// Spinner represents a loading spinner.\n// To get started simply create a new spinner and call `Run`.\n//\n//\ts := spinner.New()\n//\ts.Run()\n//\n// ⣾  Loading...\ntype Spinner struct {\n\tspinner    spinner.Model\n\taction     func(ctx context.Context) error\n\tctx        context.Context\n\taccessible bool\n\ttitle      string\n\terr        error\n\tteaOptions []tea.ProgramOption\n\tviewHook   compat.ViewHook\n\ttheme      Theme\n\toutput     io.Writer // acessible mode output\n\tinput      io.Reader // acessible mode output\n\thasDarkBg  bool\n}\n\n// Styles are the spinner styles.\ntype Styles struct {\n\tSpinner, Title lipgloss.Style\n}\n\n// Theme represents a theme for a huh.\ntype Theme interface {\n\tTheme(isDark bool) *Styles\n}\n\n// ThemeFunc is a function that returns a new theme.\ntype ThemeFunc func(isDark bool) *Styles\n\n// Theme implements the Theme interface.\nfunc (f ThemeFunc) Theme(isDark bool) *Styles {\n\treturn f(isDark)\n}\n\n// ThemeDefault is the default theme.\nfunc ThemeDefault(isDark bool) *Styles {\n\tlightDark := lipgloss.LightDark(isDark)\n\ttitle := lightDark(\n\t\tlipgloss.Color(\"#00020A\"),\n\t\tlipgloss.Color(\"#FFFDF5\"),\n\t)\n\treturn &Styles{\n\t\tSpinner: lipgloss.NewStyle().Foreground(lipgloss.Color(\"#F780E2\")),\n\t\tTitle:   lipgloss.NewStyle().Foreground(title),\n\t}\n}\n\n// Type is a set of frames used in animating the spinner.\ntype Type spinner.Spinner\n\n// Spinner [Type]s.\nvar (\n\tLine      = Type(spinner.Line)\n\tDots      = Type(spinner.Dot)\n\tMiniDot   = Type(spinner.MiniDot)\n\tJump      = Type(spinner.Jump)\n\tPoints    = Type(spinner.Points)\n\tPulse     = Type(spinner.Pulse)\n\tGlobe     = Type(spinner.Globe)\n\tMoon      = Type(spinner.Moon)\n\tMonkey    = Type(spinner.Monkey)\n\tMeter     = Type(spinner.Meter)\n\tHamburger = Type(spinner.Hamburger)\n\tEllipsis  = Type(spinner.Ellipsis)\n)\n\n// Type sets the type of the spinner.\nfunc (s *Spinner) Type(t Type) *Spinner {\n\ts.spinner.Spinner = spinner.Spinner(t)\n\treturn s\n}\n\n// Title sets the title of the spinner.\nfunc (s *Spinner) Title(title string) *Spinner {\n\ts.title = title\n\treturn s\n}\n\n// WithOutput set the output for the spinner.\n// Default is STDOUT when [Spinner.WithAccessible], STDERR otherwise.\nfunc (s *Spinner) WithOutput(w io.Writer) *Spinner {\n\ts.teaOptions = append(s.teaOptions, tea.WithOutput(w))\n\ts.output = w\n\treturn s\n}\n\n// WithInput set the input for the spinner.\n// Default is STDIN.\nfunc (s *Spinner) WithInput(r io.Reader) *Spinner {\n\ts.teaOptions = append(s.teaOptions, tea.WithInput(r))\n\ts.input = r\n\treturn s\n}\n\n// WithViewHook allows to set a [compat.ViewHook].\nfunc (s *Spinner) WithViewHook(hook compat.ViewHook) *Spinner {\n\ts.viewHook = hook\n\treturn s\n}\n\n// Action sets the action of the spinner.\nfunc (s *Spinner) Action(action func()) *Spinner {\n\ts.action = func(context.Context) error {\n\t\taction()\n\t\treturn nil\n\t}\n\treturn s\n}\n\n// ActionWithErr sets the action of the spinner.\n//\n// This is just like [Spinner.Action], but allows the action to use a `context.Context`\n// and to return an error.\nfunc (s *Spinner) ActionWithErr(action func(context.Context) error) *Spinner {\n\ts.action = action\n\treturn s\n}\n\n// Context sets the context of the spinner.\nfunc (s *Spinner) Context(ctx context.Context) *Spinner {\n\ts.ctx = ctx\n\treturn s\n}\n\n// WithAccessible sets the spinner to be static.\nfunc (s *Spinner) WithAccessible(accessible bool) *Spinner {\n\ts.accessible = accessible\n\treturn s\n}\n\n// New creates a new spinner.\nfunc New() *Spinner {\n\ts := spinner.New()\n\ts.Spinner = spinner.Dot\n\treturn &Spinner{\n\t\tspinner: s,\n\t\ttitle:   \"Loading...\",\n\t\ttheme:   ThemeFunc(ThemeDefault),\n\t}\n}\n\n// WithTheme sets the theme for the spinner.\nfunc (s *Spinner) WithTheme(theme Theme) *Spinner {\n\tif theme == nil {\n\t\treturn s\n\t}\n\n\ts.theme = theme\n\treturn s\n}\n\n// Init initializes the spinner.\nfunc (s *Spinner) Init() tea.Cmd {\n\treturn tea.Batch(\n\t\ttea.RequestBackgroundColor,\n\t\ts.spinner.Tick,\n\t\tfunc() tea.Msg {\n\t\t\tif s.action != nil {\n\t\t\t\terr := s.action(s.ctx)\n\t\t\t\treturn doneMsg{err}\n\t\t\t}\n\t\t\treturn nil\n\t\t},\n\t)\n}\n\n// Update updates the spinner.\nfunc (s *Spinner) Update(msg tea.Msg) (Model, tea.Cmd) {\n\tswitch msg := msg.(type) {\n\tcase tea.BackgroundColorMsg:\n\t\ts.hasDarkBg = msg.IsDark()\n\tcase doneMsg:\n\t\ts.err = msg.err\n\t\treturn s, tea.Quit\n\tcase tea.KeyPressMsg:\n\t\tswitch msg.String() {\n\t\tcase \"ctrl+c\":\n\t\t\treturn s, tea.Interrupt\n\t\t}\n\t}\n\n\tvar cmd tea.Cmd\n\ts.spinner, cmd = s.spinner.Update(msg)\n\treturn s, cmd\n}\n\n// View returns the spinner view.\nfunc (s *Spinner) View() string {\n\tstyles := s.theme.Theme(s.hasDarkBg)\n\ts.spinner.Style = styles.Spinner\n\tvar title string\n\tif s.title != \"\" {\n\t\ttitle = styles.Title.Render(s.title)\n\t}\n\treturn s.spinner.View() + title\n}\n\n// Run runs the spinner.\nfunc (s *Spinner) Run() error {\n\tif s.ctx == nil && s.action == nil {\n\t\treturn nil\n\t}\n\tif s.ctx == nil {\n\t\ts.ctx = context.Background()\n\t}\n\tif err := s.ctx.Err(); err != nil {\n\t\treturn err //nolint:wrapcheck\n\t}\n\n\tif s.accessible {\n\t\tout := cmp.Or[io.Writer](s.output, os.Stdout)\n\t\tin := cmp.Or[io.Reader](s.input, os.Stdin)\n\t\treturn s.runAccessible(in, out)\n\t}\n\n\topts := append(s.teaOptions, tea.WithContext(s.ctx))\n\tif s.output != nil {\n\t\topts = append(opts, tea.WithOutput(s.output))\n\t}\n\tif s.input != nil {\n\t\topts = append(opts, tea.WithInput(s.input))\n\t}\n\tm, err := tea.NewProgram(compat.ViewModel{\n\t\tModel:    s,\n\t\tViewHook: s.viewHook,\n\t}, opts...).Run()\n\tmm := m.(compat.ViewModel).Model.(*Spinner)\n\tif mm.err != nil {\n\t\treturn mm.err\n\t}\n\treturn err //nolint:wrapcheck\n}\n\n// runAccessible runs the spinner in an accessible mode (statically).\nfunc (s *Spinner) runAccessible(in io.Reader, out io.Writer) error {\n\ttin, iok := in.(term.File)\n\ttout, ook := out.(term.File)\n\n\ts.hasDarkBg = true\n\tif iok && ook {\n\t\ts.hasDarkBg = lipgloss.HasDarkBackground(tin, tout)\n\t}\n\n\tstyles := s.theme.Theme(s.hasDarkBg)\n\n\t_, _ = io.WriteString(out, ansi.HideCursor)\n\tframe := s.spinner.Style.Render(\"...\")\n\ttitle := styles.Title.Render(strings.TrimSuffix(s.title, \"...\"))\n\t_, _ = io.WriteString(out, title+frame)\n\n\tdefer func() {\n\t\t_, _ = io.WriteString(out, ansi.ShowCursor)\n\t}()\n\n\tactionDone := make(chan error)\n\tif s.action != nil {\n\t\tgo func() {\n\t\t\tactionDone <- s.action(s.ctx)\n\t\t}()\n\t}\n\n\tfor {\n\t\tselect {\n\t\tcase <-s.ctx.Done():\n\t\t\treturn s.ctx.Err() //nolint:wrapcheck\n\t\tcase err := <-actionDone:\n\t\t\treturn err\n\t\t}\n\t}\n}\n\ntype doneMsg struct {\n\terr error\n}\n"
  },
  {
    "path": "spinner/spinner_test.go",
    "content": "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/spinner\"\n\ttea \"charm.land/bubbletea/v2\"\n\t\"charm.land/lipgloss/v2\"\n)\n\nfunc TestNewSpinner(t *testing.T) {\n\ts := New()\n\tif s.title != \"Loading...\" {\n\t\tt.Errorf(\"Expected default title 'Loading...', got '%s'\", s.title)\n\t}\n\tif !reflect.DeepEqual(s.spinner.Spinner, spinner.Dot) {\n\t\tt.Errorf(\"Expected default spinner type to be Dot, got %v\", s.spinner.Spinner)\n\t}\n}\n\nfunc TestSpinnerType(t *testing.T) {\n\ts := New().Type(Dots)\n\tif !reflect.DeepEqual(s.spinner.Spinner, spinner.Dot) {\n\t\tt.Errorf(\"Expected spinner type to be Dot, got %v\", s.spinner.Spinner)\n\t}\n}\n\nfunc TestSpinnerDifferentTypes(t *testing.T) {\n\ts := New().Type(Line)\n\tif !reflect.DeepEqual(s.spinner.Spinner, spinner.Line) {\n\t\tt.Errorf(\"Expected spinner type to be Line, got %v\", s.spinner.Spinner)\n\t}\n}\n\nfunc TestSpinnerView(t *testing.T) {\n\ts := New().Title(\"Test\")\n\tview := s.View()\n\n\tif !strings.Contains(view, \"Test\") {\n\t\tt.Errorf(\"Expected view to contain title 'Test', got '%s'\", view)\n\t}\n}\n\nfunc TestSpinnerContextCancellation(t *testing.T) {\n\texercise(t, func() *Spinner {\n\t\tctx, cancel := context.WithCancel(context.Background())\n\t\ts := New().Context(ctx)\n\t\tcancel() // Cancel before running\n\t\treturn s\n\t}, requireContextCanceled)\n}\n\nfunc TestSpinnerContextCancellationWhileRunning(t *testing.T) {\n\texercise(t, func() *Spinner {\n\t\tctx, cancel := context.WithCancel(context.Background())\n\t\tgo func() {\n\t\t\ttime.Sleep(250 * time.Millisecond)\n\t\t\tcancel()\n\t\t}()\n\t\treturn New().Context(ctx)\n\t}, requireContextCanceled)\n}\n\nfunc TestSpinnerStyleMethods(t *testing.T) {\n\ts := New()\n\n\ttheme := ThemeFunc(func(bool) *Styles {\n\t\treturn &Styles{\n\t\t\tSpinner: lipgloss.NewStyle().Foreground(lipgloss.Color(\"red\")),\n\t\t\tTitle:   lipgloss.NewStyle().Foreground(lipgloss.Color(\"blue\")),\n\t\t}\n\t})\n\n\ts.WithTheme(theme).View()\n\tstyles := s.theme.Theme(true)\n\tif !reflect.DeepEqual(s.spinner.Style, styles.Spinner) {\n\t\tt.Errorf(\"Style was not set correctly\")\n\t}\n}\n\nfunc TestSpinnerInit(t *testing.T) {\n\ts := New()\n\tcmd := s.Init()\n\n\tif cmd == nil {\n\t\tt.Errorf(\"Init did not return a valid command\")\n\t}\n}\n\nfunc TestSpinnerUpdate(t *testing.T) {\n\ts := New()\n\tcmd := s.Init()\n\tif cmd == nil {\n\t\tt.Errorf(\"Init did not return a valid command\")\n\t}\n\n\tmodel, cmd := s.Update(spinner.TickMsg{})\n\tif reflect.TypeOf(model) != reflect.TypeOf(&Spinner{}) {\n\t\tt.Errorf(\"Update did not return correct model type\")\n\t}\n\n\tif cmd == nil {\n\t\tt.Errorf(\"Update should return a non-nil command in this scenario\")\n\t}\n\n\t// Simulate key press\n\t_, cmd = s.Update(tea.KeyPressMsg(tea.Key{Mod: tea.ModCtrl, Code: 'c'}))\n\tif cmd == nil {\n\t\tt.Errorf(\"Update did not handle key press correctly\")\n\t}\n}\n\nfunc TestSpinnerSimple(t *testing.T) {\n\texercise(t, func() *Spinner {\n\t\treturn New().Action(func() {})\n\t}, requireNoError)\n}\n\nfunc TestSpinnerWithContextAndAction(t *testing.T) {\n\texercise(t, func() *Spinner {\n\t\tctx := context.Background()\n\t\treturn New().Context(ctx).Action(func() {})\n\t}, requireNoError)\n}\n\nfunc TestSpinnerWithActionError(t *testing.T) {\n\tfake := errors.New(\"fake\")\n\texercise(t, func() *Spinner {\n\t\treturn New().ActionWithErr(func(context.Context) error { return fake })\n\t}, requireErrorIs(fake))\n}\n\nfunc exercise(t *testing.T, factory func() *Spinner, checker func(tb testing.TB, err error)) {\n\tt.Helper()\n\tt.Run(\"accessible\", func(t *testing.T) {\n\t\terr := factory().\n\t\t\tWithAccessible(true).\n\t\t\tWithOutput(io.Discard).\n\t\t\tWithInput(nilReader{}).\n\t\t\tRun()\n\t\tchecker(t, err)\n\t})\n\tt.Run(\"regular\", func(t *testing.T) {\n\t\terr := factory().\n\t\t\tWithAccessible(false).\n\t\t\tWithOutput(io.Discard).\n\t\t\tWithInput(nilReader{}).\n\t\t\tRun()\n\t\tchecker(t, err)\n\t})\n}\n\nfunc requireNoError(tb testing.TB, err error) {\n\ttb.Helper()\n\tif err != nil {\n\t\ttb.Errorf(\"expected no error, got %v\", err)\n\t}\n}\n\nfunc requireErrorIs(target error) func(tb testing.TB, err error) {\n\treturn func(tb testing.TB, err error) {\n\t\ttb.Helper()\n\t\tif !errors.Is(err, target) {\n\t\t\ttb.Errorf(\"expected error to be %v, got %v\", target, err)\n\t\t}\n\t}\n}\n\nfunc requireContextCanceled(tb testing.TB, err error) {\n\ttb.Helper()\n\tswitch {\n\tcase errors.Is(err, context.Canceled):\n\tcase errors.Is(err, tea.ErrProgramKilled):\n\tdefault:\n\t\ttb.Errorf(\"expected to get a context canceled error, got %v\", err)\n\t}\n}\n\ntype nilReader struct{}\n\n// Read implements io.Reader.\nfunc (nilReader) Read([]byte) (int, error) { return 0, nil }\n"
  },
  {
    "path": "theme.go",
    "content": "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// Theme represents a theme for a huh.\ntype Theme interface {\n\tTheme(isDark bool) *Styles\n}\n\n// ThemeFunc is a function that returns a new theme.\ntype ThemeFunc func(isDark bool) *Styles\n\n// Theme implements the Theme interface.\nfunc (f ThemeFunc) Theme(isDark bool) *Styles {\n\treturn f(isDark)\n}\n\n// Styles is a collection of styles for components of the form.\n// Themes can be applied to a form using the WithTheme option.\ntype Styles struct {\n\tForm           FormStyles\n\tGroup          GroupStyles\n\tFieldSeparator lipgloss.Style\n\tBlurred        FieldStyles\n\tFocused        FieldStyles\n\tHelp           help.Styles\n}\n\n// FormStyles are the styles for a form.\ntype FormStyles struct {\n\tBase lipgloss.Style\n}\n\n// GroupStyles are the styles for a group.\ntype GroupStyles struct {\n\tBase        lipgloss.Style\n\tTitle       lipgloss.Style\n\tDescription lipgloss.Style\n}\n\n// FieldStyles are the styles for input fields.\ntype FieldStyles struct {\n\tBase           lipgloss.Style\n\tTitle          lipgloss.Style\n\tDescription    lipgloss.Style\n\tErrorIndicator lipgloss.Style\n\tErrorMessage   lipgloss.Style\n\n\t// Select styles.\n\tSelectSelector lipgloss.Style // Selection indicator\n\tOption         lipgloss.Style // Select options\n\tNextIndicator  lipgloss.Style\n\tPrevIndicator  lipgloss.Style\n\n\t// FilePicker styles.\n\tDirectory lipgloss.Style\n\tFile      lipgloss.Style\n\n\t// Multi-select styles.\n\tMultiSelectSelector lipgloss.Style\n\tSelectedOption      lipgloss.Style\n\tSelectedPrefix      lipgloss.Style\n\tUnselectedOption    lipgloss.Style\n\tUnselectedPrefix    lipgloss.Style\n\n\t// Textinput and teatarea styles.\n\tTextInput TextInputStyles\n\n\t// Confirm styles.\n\tFocusedButton lipgloss.Style\n\tBlurredButton lipgloss.Style\n\n\t// Card styles.\n\tCard      lipgloss.Style\n\tNoteTitle lipgloss.Style\n\tNext      lipgloss.Style\n}\n\n// TextInputStyles are the styles for text inputs.\ntype TextInputStyles struct {\n\tCursor      lipgloss.Style\n\tCursorText  lipgloss.Style\n\tPlaceholder lipgloss.Style\n\tPrompt      lipgloss.Style\n\tText        lipgloss.Style\n}\n\nconst (\n\tbuttonPaddingHorizontal = 2\n\tbuttonPaddingVertical   = 0\n)\n\n// ThemeBase returns a new base theme with general styles to be inherited by\n// other themes.\nfunc ThemeBase(bool) *Styles {\n\tvar t Styles\n\n\tt.Form.Base = lipgloss.NewStyle()\n\tt.Group.Base = lipgloss.NewStyle()\n\tt.FieldSeparator = lipgloss.NewStyle().SetString(\"\\n\\n\")\n\n\tbutton := lipgloss.NewStyle().\n\t\tPadding(buttonPaddingVertical, buttonPaddingHorizontal).\n\t\tMarginRight(1)\n\n\t// Focused styles.\n\tt.Focused.Base = lipgloss.NewStyle().PaddingLeft(1).BorderStyle(lipgloss.ThickBorder()).BorderLeft(true)\n\tt.Focused.Card = t.Focused.Base\n\tt.Focused.ErrorIndicator = lipgloss.NewStyle().SetString(\" *\")\n\tt.Focused.ErrorMessage = lipgloss.NewStyle().SetString(\" *\")\n\tt.Focused.SelectSelector = lipgloss.NewStyle().SetString(\"> \")\n\tt.Focused.NextIndicator = lipgloss.NewStyle().MarginLeft(1).SetString(\"→\")\n\tt.Focused.PrevIndicator = lipgloss.NewStyle().MarginRight(1).SetString(\"←\")\n\tt.Focused.MultiSelectSelector = lipgloss.NewStyle().SetString(\"> \")\n\tt.Focused.SelectedPrefix = lipgloss.NewStyle().SetString(\"[•] \")\n\tt.Focused.UnselectedPrefix = lipgloss.NewStyle().SetString(\"[ ] \")\n\tt.Focused.FocusedButton = button.Foreground(lipgloss.Color(\"0\")).Background(lipgloss.Color(\"7\"))\n\tt.Focused.BlurredButton = button.Foreground(lipgloss.Color(\"7\")).Background(lipgloss.Color(\"0\"))\n\tt.Focused.TextInput.Placeholder = lipgloss.NewStyle().Foreground(lipgloss.Color(\"8\"))\n\n\tt.Help = help.New().Styles\n\n\t// Blurred styles.\n\tt.Blurred = t.Focused\n\tt.Blurred.Base = t.Blurred.Base.BorderStyle(lipgloss.HiddenBorder())\n\tt.Blurred.Card = t.Blurred.Base\n\tt.Blurred.MultiSelectSelector = lipgloss.NewStyle().SetString(\"  \")\n\tt.Blurred.NextIndicator = lipgloss.NewStyle()\n\tt.Blurred.PrevIndicator = lipgloss.NewStyle()\n\n\treturn &t\n}\n\n// ThemeCharm returns a new theme based on the Charm color scheme.\nfunc ThemeCharm(isDark bool) *Styles {\n\tt := ThemeBase(isDark)\n\tlightDark := lipgloss.LightDark(isDark)\n\n\tvar (\n\t\tnormalFg = lightDark(lipgloss.Color(\"252\"), lipgloss.Color(\"235\"))\n\t\tindigo   = lightDark(lipgloss.Color(\"#5A56E0\"), lipgloss.Color(\"#7571F9\"))\n\t\tcream    = lightDark(lipgloss.Color(\"#FFFDF5\"), lipgloss.Color(\"#FFFDF5\"))\n\t\tfuchsia  = lipgloss.Color(\"#F780E2\")\n\t\tgreen    = lightDark(lipgloss.Color(\"#02BA84\"), lipgloss.Color(\"#02BF87\"))\n\t\tred      = lightDark(lipgloss.Color(\"#FF4672\"), lipgloss.Color(\"#ED567A\"))\n\t)\n\n\tt.Focused.Base = t.Focused.Base.BorderForeground(lipgloss.Color(\"238\"))\n\tt.Focused.Card = t.Focused.Base\n\tt.Focused.Title = t.Focused.Title.Foreground(indigo).Bold(true)\n\tt.Focused.NoteTitle = t.Focused.NoteTitle.Foreground(indigo).Bold(true).MarginBottom(1)\n\tt.Focused.Directory = t.Focused.Directory.Foreground(indigo)\n\tt.Focused.Description = t.Focused.Description.Foreground(lightDark(lipgloss.Color(\"\"), lipgloss.Color(\"243\")))\n\tt.Focused.ErrorIndicator = t.Focused.ErrorIndicator.Foreground(red)\n\tt.Focused.ErrorMessage = t.Focused.ErrorMessage.Foreground(red)\n\tt.Focused.SelectSelector = t.Focused.SelectSelector.Foreground(fuchsia)\n\tt.Focused.NextIndicator = t.Focused.NextIndicator.Foreground(fuchsia)\n\tt.Focused.PrevIndicator = t.Focused.PrevIndicator.Foreground(fuchsia)\n\tt.Focused.Option = t.Focused.Option.Foreground(normalFg)\n\tt.Focused.MultiSelectSelector = t.Focused.MultiSelectSelector.Foreground(fuchsia)\n\tt.Focused.SelectedOption = t.Focused.SelectedOption.Foreground(green)\n\tt.Focused.SelectedPrefix = lipgloss.NewStyle().Foreground(lightDark(lipgloss.Color(\"#02CF92\"), lipgloss.Color(\"#02A877\"))).SetString(\"✓ \")\n\tt.Focused.UnselectedPrefix = lipgloss.NewStyle().Foreground(lightDark(lipgloss.Color(\"\"), lipgloss.Color(\"243\"))).SetString(\"• \")\n\tt.Focused.UnselectedOption = t.Focused.UnselectedOption.Foreground(normalFg)\n\tt.Focused.FocusedButton = t.Focused.FocusedButton.Foreground(cream).Background(fuchsia)\n\tt.Focused.Next = t.Focused.FocusedButton\n\tt.Focused.BlurredButton = t.Focused.BlurredButton.Foreground(normalFg).Background(lightDark(lipgloss.Color(\"237\"), lipgloss.Color(\"252\")))\n\n\tt.Focused.TextInput.Cursor = t.Focused.TextInput.Cursor.Foreground(green)\n\tt.Focused.TextInput.Placeholder = t.Focused.TextInput.Placeholder.Foreground(lightDark(lipgloss.Color(\"248\"), lipgloss.Color(\"238\")))\n\tt.Focused.TextInput.Prompt = t.Focused.TextInput.Prompt.Foreground(fuchsia)\n\n\tt.Blurred = t.Focused\n\tt.Blurred.Base = t.Focused.Base.BorderStyle(lipgloss.HiddenBorder())\n\tt.Blurred.Card = t.Blurred.Base\n\tt.Blurred.NextIndicator = lipgloss.NewStyle()\n\tt.Blurred.PrevIndicator = lipgloss.NewStyle()\n\n\tt.Group.Title = t.Focused.Title\n\tt.Group.Description = t.Focused.Description\n\treturn t\n}\n\n// ThemeDracula returns a new theme based on the Dracula color scheme.\nfunc ThemeDracula(isDark bool) *Styles {\n\tt := ThemeBase(isDark)\n\n\tvar (\n\t\tbackground = lipgloss.Color(\"#282a36\")\n\t\tselection  = lipgloss.Color(\"#44475a\")\n\t\tforeground = lipgloss.Color(\"#f8f8f2\")\n\t\tcomment    = lipgloss.Color(\"#6272a4\")\n\t\tgreen      = lipgloss.Color(\"#50fa7b\")\n\t\tpurple     = lipgloss.Color(\"#bd93f9\")\n\t\tred        = lipgloss.Color(\"#ff5555\")\n\t\tyellow     = lipgloss.Color(\"#f1fa8c\")\n\t)\n\n\tt.Focused.Base = t.Focused.Base.BorderForeground(selection)\n\tt.Focused.Card = t.Focused.Base\n\tt.Focused.Title = t.Focused.Title.Foreground(purple)\n\tt.Focused.NoteTitle = t.Focused.NoteTitle.Foreground(purple)\n\tt.Focused.Description = t.Focused.Description.Foreground(comment)\n\tt.Focused.ErrorIndicator = t.Focused.ErrorIndicator.Foreground(red)\n\tt.Focused.Directory = t.Focused.Directory.Foreground(purple)\n\tt.Focused.File = t.Focused.File.Foreground(foreground)\n\tt.Focused.ErrorMessage = t.Focused.ErrorMessage.Foreground(red)\n\tt.Focused.SelectSelector = t.Focused.SelectSelector.Foreground(yellow)\n\tt.Focused.NextIndicator = t.Focused.NextIndicator.Foreground(yellow)\n\tt.Focused.PrevIndicator = t.Focused.PrevIndicator.Foreground(yellow)\n\tt.Focused.Option = t.Focused.Option.Foreground(foreground)\n\tt.Focused.MultiSelectSelector = t.Focused.MultiSelectSelector.Foreground(yellow)\n\tt.Focused.SelectedOption = t.Focused.SelectedOption.Foreground(green)\n\tt.Focused.SelectedPrefix = t.Focused.SelectedPrefix.Foreground(green)\n\tt.Focused.UnselectedOption = t.Focused.UnselectedOption.Foreground(foreground)\n\tt.Focused.UnselectedPrefix = t.Focused.UnselectedPrefix.Foreground(comment)\n\tt.Focused.FocusedButton = t.Focused.FocusedButton.Foreground(yellow).Background(purple).Bold(true)\n\tt.Focused.BlurredButton = t.Focused.BlurredButton.Foreground(foreground).Background(background)\n\n\tt.Focused.TextInput.Cursor = t.Focused.TextInput.Cursor.Foreground(yellow)\n\tt.Focused.TextInput.Placeholder = t.Focused.TextInput.Placeholder.Foreground(comment)\n\tt.Focused.TextInput.Prompt = t.Focused.TextInput.Prompt.Foreground(yellow)\n\n\tt.Blurred = t.Focused\n\tt.Blurred.Base = t.Blurred.Base.BorderStyle(lipgloss.HiddenBorder())\n\tt.Blurred.Card = t.Blurred.Base\n\tt.Blurred.NextIndicator = lipgloss.NewStyle()\n\tt.Blurred.PrevIndicator = lipgloss.NewStyle()\n\n\tt.Group.Title = t.Focused.Title\n\tt.Group.Description = t.Focused.Description\n\treturn t\n}\n\n// ThemeBase16 returns a new theme based on the base16 color scheme.\nfunc ThemeBase16(isDark bool) *Styles {\n\tt := ThemeBase(isDark)\n\n\tt.Focused.Base = t.Focused.Base.BorderForeground(lipgloss.Color(\"8\"))\n\tt.Focused.Card = t.Focused.Base\n\tt.Focused.Title = t.Focused.Title.Foreground(lipgloss.Color(\"6\"))\n\tt.Focused.NoteTitle = t.Focused.NoteTitle.Foreground(lipgloss.Color(\"6\"))\n\tt.Focused.Directory = t.Focused.Directory.Foreground(lipgloss.Color(\"6\"))\n\tt.Focused.Description = t.Focused.Description.Foreground(lipgloss.Color(\"8\"))\n\tt.Focused.ErrorIndicator = t.Focused.ErrorIndicator.Foreground(lipgloss.Color(\"9\"))\n\tt.Focused.ErrorMessage = t.Focused.ErrorMessage.Foreground(lipgloss.Color(\"9\"))\n\tt.Focused.SelectSelector = t.Focused.SelectSelector.Foreground(lipgloss.Color(\"3\"))\n\tt.Focused.NextIndicator = t.Focused.NextIndicator.Foreground(lipgloss.Color(\"3\"))\n\tt.Focused.PrevIndicator = t.Focused.PrevIndicator.Foreground(lipgloss.Color(\"3\"))\n\tt.Focused.Option = t.Focused.Option.Foreground(lipgloss.Color(\"7\"))\n\tt.Focused.MultiSelectSelector = t.Focused.MultiSelectSelector.Foreground(lipgloss.Color(\"3\"))\n\tt.Focused.SelectedOption = t.Focused.SelectedOption.Foreground(lipgloss.Color(\"2\"))\n\tt.Focused.SelectedPrefix = t.Focused.SelectedPrefix.Foreground(lipgloss.Color(\"2\"))\n\tt.Focused.UnselectedOption = t.Focused.UnselectedOption.Foreground(lipgloss.Color(\"7\"))\n\tt.Focused.FocusedButton = t.Focused.FocusedButton.Foreground(lipgloss.Color(\"7\")).Background(lipgloss.Color(\"5\"))\n\tt.Focused.BlurredButton = t.Focused.BlurredButton.Foreground(lipgloss.Color(\"7\")).Background(lipgloss.Color(\"0\"))\n\n\tt.Focused.TextInput.Cursor.Foreground(lipgloss.Color(\"5\"))\n\tt.Focused.TextInput.Placeholder.Foreground(lipgloss.Color(\"8\"))\n\tt.Focused.TextInput.Prompt.Foreground(lipgloss.Color(\"3\"))\n\n\tt.Blurred = t.Focused\n\tt.Blurred.Base = t.Blurred.Base.BorderStyle(lipgloss.HiddenBorder())\n\tt.Blurred.Card = t.Blurred.Base\n\tt.Blurred.NoteTitle = t.Blurred.NoteTitle.Foreground(lipgloss.Color(\"8\"))\n\tt.Blurred.Title = t.Blurred.NoteTitle.Foreground(lipgloss.Color(\"8\"))\n\n\tt.Blurred.TextInput.Prompt = t.Blurred.TextInput.Prompt.Foreground(lipgloss.Color(\"8\"))\n\tt.Blurred.TextInput.Text = t.Blurred.TextInput.Text.Foreground(lipgloss.Color(\"7\"))\n\n\tt.Blurred.NextIndicator = lipgloss.NewStyle()\n\tt.Blurred.PrevIndicator = lipgloss.NewStyle()\n\n\tt.Group.Title = t.Focused.Title\n\tt.Group.Description = t.Focused.Description\n\n\treturn t\n}\n\n// ThemeCatppuccin returns a new theme based on the Catppuccin color scheme.\nfunc ThemeCatppuccin(isDark bool) *Styles {\n\tt := ThemeBase(isDark)\n\n\tflavour := catppuccin.Latte\n\tif isDark {\n\t\tflavour = catppuccin.Mocha\n\t}\n\tvar (\n\t\tbase     = flavour.Base()\n\t\ttext     = flavour.Text()\n\t\tsubtext1 = flavour.Subtext1()\n\t\tsubtext0 = flavour.Subtext0()\n\t\toverlay1 = flavour.Overlay1()\n\t\toverlay0 = flavour.Overlay0()\n\t\tgreen    = flavour.Green()\n\t\tred      = flavour.Red()\n\t\tpink     = flavour.Pink()\n\t\tmauve    = flavour.Mauve()\n\t\tcursor   = flavour.Rosewater()\n\t)\n\n\tt.Focused.Base = t.Focused.Base.BorderForeground(subtext1)\n\tt.Focused.Card = t.Focused.Base\n\tt.Focused.Title = t.Focused.Title.Foreground(mauve)\n\tt.Focused.NoteTitle = t.Focused.NoteTitle.Foreground(mauve)\n\tt.Focused.Directory = t.Focused.Directory.Foreground(mauve)\n\tt.Focused.Description = t.Focused.Description.Foreground(subtext0)\n\tt.Focused.ErrorIndicator = t.Focused.ErrorIndicator.Foreground(red)\n\tt.Focused.ErrorMessage = t.Focused.ErrorMessage.Foreground(red)\n\tt.Focused.SelectSelector = t.Focused.SelectSelector.Foreground(pink)\n\tt.Focused.NextIndicator = t.Focused.NextIndicator.Foreground(pink)\n\tt.Focused.PrevIndicator = t.Focused.PrevIndicator.Foreground(pink)\n\tt.Focused.Option = t.Focused.Option.Foreground(text)\n\tt.Focused.MultiSelectSelector = t.Focused.MultiSelectSelector.Foreground(pink)\n\tt.Focused.SelectedOption = t.Focused.SelectedOption.Foreground(green)\n\tt.Focused.SelectedPrefix = t.Focused.SelectedPrefix.Foreground(green)\n\tt.Focused.UnselectedPrefix = t.Focused.UnselectedPrefix.Foreground(text)\n\tt.Focused.UnselectedOption = t.Focused.UnselectedOption.Foreground(text)\n\tt.Focused.FocusedButton = t.Focused.FocusedButton.Foreground(base).Background(pink)\n\tt.Focused.BlurredButton = t.Focused.BlurredButton.Foreground(text).Background(base)\n\n\tt.Focused.TextInput.Cursor = t.Focused.TextInput.Cursor.Foreground(cursor)\n\tt.Focused.TextInput.Placeholder = t.Focused.TextInput.Placeholder.Foreground(overlay0)\n\tt.Focused.TextInput.Prompt = t.Focused.TextInput.Prompt.Foreground(pink)\n\n\tt.Blurred = t.Focused\n\tt.Blurred.Base = t.Blurred.Base.BorderStyle(lipgloss.HiddenBorder())\n\tt.Blurred.Card = t.Blurred.Base\n\n\tt.Help.Ellipsis = t.Help.Ellipsis.Foreground(subtext0)\n\tt.Help.ShortKey = t.Help.ShortKey.Foreground(subtext0)\n\tt.Help.ShortDesc = t.Help.ShortDesc.Foreground(overlay1)\n\tt.Help.ShortSeparator = t.Help.ShortSeparator.Foreground(subtext0)\n\tt.Help.FullKey = t.Help.FullKey.Foreground(subtext0)\n\tt.Help.FullDesc = t.Help.FullDesc.Foreground(overlay1)\n\tt.Help.FullSeparator = t.Help.FullSeparator.Foreground(subtext0)\n\n\tt.Group.Title = t.Focused.Title\n\tt.Group.Description = t.Focused.Description\n\treturn t\n}\n"
  },
  {
    "path": "validate.go",
    "content": "package huh\n\nimport (\n\t\"fmt\"\n\t\"unicode/utf8\"\n)\n\n// ValidateNotEmpty checks if the input is not empty.\nfunc ValidateNotEmpty() func(s string) error {\n\treturn func(s string) error {\n\t\tif err := ValidateMinLength(1)(s); err != nil {\n\t\t\treturn fmt.Errorf(\"input cannot be empty\")\n\t\t}\n\t\treturn nil\n\t}\n}\n\n// ValidateMinLength checks if the length of the input is at least min.\nfunc ValidateMinLength(v int) func(s string) error {\n\treturn func(s string) error {\n\t\tif utf8.RuneCountInString(s) < v {\n\t\t\treturn fmt.Errorf(\"input must be at least %d characters long\", v)\n\t\t}\n\t\treturn nil\n\t}\n}\n\n// ValidateMaxLength checks if the length of the input is at most max.\nfunc ValidateMaxLength(v int) func(s string) error {\n\treturn func(s string) error {\n\t\tif utf8.RuneCountInString(s) > v {\n\t\t\treturn fmt.Errorf(\"input must be at most %d characters long\", v)\n\t\t}\n\t\treturn nil\n\t}\n}\n\n// ValidateLength checks if the length of the input is within the specified range.\nfunc ValidateLength(minl, maxl int) func(s string) error {\n\treturn func(s string) error {\n\t\tif err := ValidateMinLength(minl)(s); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn ValidateMaxLength(maxl)(s)\n\t}\n}\n\n// ValidateOneOf checks if a string is one of the specified options.\nfunc ValidateOneOf(options ...string) func(string) error {\n\tvalidOptions := make(map[string]struct{})\n\tfor _, option := range options {\n\t\tvalidOptions[option] = struct{}{}\n\t}\n\n\treturn func(value string) error {\n\t\tif _, ok := validOptions[value]; !ok {\n\t\t\treturn fmt.Errorf(\"invalid option: %s\", value)\n\t\t}\n\t\treturn nil\n\t}\n}\n"
  },
  {
    "path": "wrap.go",
    "content": "package huh\n\nimport \"charm.land/lipgloss/v2\"\n\nfunc wrap(s string, limit int) string {\n\treturn lipgloss.Wrap(s, limit, \",.-; \")\n}\n"
  },
  {
    "path": "zz_resize_width_test.go",
    "content": "package huh\n\nimport \"testing\"\n\nfunc TestSelectWithWidthUpdatesViewportWidth(t *testing.T) {\n\tf := NewSelect[string]().\n\t\tTitle(\"Pick one\").\n\t\tOptions(\n\t\t\tNewOption(\"Option 1\", \"1\"),\n\t\t\tNewOption(\"Option 2\", \"2\"),\n\t\t)\n\n\tf.WithWidth(18)\n\tif got, want := f.viewport.Width(), 18; got != want {\n\t\tt.Fatalf(\"viewport width after first WithWidth = %d, want %d\", got, want)\n\t}\n\n\tf.WithWidth(42)\n\tif got, want := f.viewport.Width(), 42; got != want {\n\t\tt.Fatalf(\"viewport width after resize WithWidth = %d, want %d\", got, want)\n\t}\n}\n\nfunc TestMultiSelectWithWidthUpdatesViewportWidth(t *testing.T) {\n\tf := NewMultiSelect[string]().\n\t\tTitle(\"Pick many\").\n\t\tOptions(\n\t\t\tNewOption(\"Option 1\", \"1\"),\n\t\t\tNewOption(\"Option 2\", \"2\"),\n\t\t)\n\n\tf.WithWidth(20)\n\tif got, want := f.viewport.Width(), 20; got != want {\n\t\tt.Fatalf(\"viewport width after first WithWidth = %d, want %d\", got, want)\n\t}\n\n\tf.WithWidth(44)\n\tif got, want := f.viewport.Width(), 44; got != want {\n\t\tt.Fatalf(\"viewport width after resize WithWidth = %d, want %d\", got, want)\n\t}\n}\n"
  }
]