Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

charmbracelet/huh - Library for building forms and prompts in the terminal #955

Open
1 task
ShellLM opened this issue Nov 23, 2024 · 1 comment
Open
1 task
Labels
CLI-UX Command Line Interface user experience and best practices Git-Repo Source code repository like gitlab or gh github gh tools like cli, Actions, Issues, Pages source-code Code snippets

Comments

@ShellLM
Copy link
Collaborator

ShellLM commented Nov 23, 2024

Huh?

Hey there! I'm Glenn!

Latest Release Go Docs Build Status

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

Running a burger form

huh? is easy to use in a standalone fashion, can be
integrated into a Bubble Tea application, and contains
a first-class accessible mode for screen readers.

The above example is running from a single Go program (source).

Tutorial

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

package main

import "github.com/charmbracelet/huh"

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

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

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

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

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

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

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

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

Finally, run the form:

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

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

And that's it! For more info see the full source for this
example as well as the docs.

If you need more dynamic forms that change based on input from previous fields,
check out the dynamic forms example.

Field Reference

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

Tip

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

var name string

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

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

Input

Prompt the user for a single line of text.

Input field

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

Text

Prompt the user for multiple lines of text.

Text field

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

Select

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

Select field

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

Multiple Select

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

Multiselect field

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

Confirm

Prompt the user to confirm (Yes or No).

Confirm field

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

Accessibility

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

Tip

We recommend setting this through an environment variable or configuration
option to allow the user to control accessibility.

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

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

Accessible cuisine form

Themes

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

  • Charm
  • Dracula
  • Catppuccin
  • Base 16
  • Default

Charm-themed form Dracula-themed form Catppuccin-themed form Base 16-themed form Default-themed form

Themes can take advantage of the full range of
Lip Gloss style options. For a high level theme reference see
the docs.

Dynamic Forms

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

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

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

var country string
var state string

Define your country select as you normally would:

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

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

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

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

Important

We have to pass &country as the binding to recompute the function only when
country changes, otherwise we will hit the API too often.

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

Lastly, run the form with these inputs.

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

Country / State form with dynamic inputs running.

Bonus: Spinner

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

Spinner while making a burger

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

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

fmt.Println("Order up!")
go makeBurger()

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

fmt.Println("Order up!")

For more on Spinners see the spinner examples and
the spinner docs.

What about Bubble Tea?

Bubbletea + Huh?

In addition to its standalone mode, huh? has first-class support for
[Bubble Tea][tea] and can be easily integrated into Bubble Tea applications.
It's incredibly useful in portions of your Bubble Tea application that need
form-like input.

Bubble Tea embedded form example

A huh.Form is merely a tea.Model, so you can use it just as
you would any other Bubble.

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

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

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

#### Suggested labels
#### None
@ShellLM ShellLM added CLI-UX Command Line Interface user experience and best practices Git-Repo Source code repository like gitlab or gh github gh tools like cli, Actions, Issues, Pages source-code Code snippets labels Nov 23, 2024
@ShellLM
Copy link
Collaborator Author

ShellLM commented Nov 23, 2024

Related content

#953 similarity score: 0.88
#762 similarity score: 0.88
#743 similarity score: 0.87
#625 similarity score: 0.87
#114 similarity score: 0.87
#725 similarity score: 0.87

@irthomasthomas irthomasthomas changed the title huh/README.md at main · charmbracelet/huh charmbracelet/huh - Library for building forms and prompts in the terminal Nov 23, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLI-UX Command Line Interface user experience and best practices Git-Repo Source code repository like gitlab or gh github gh tools like cli, Actions, Issues, Pages source-code Code snippets
Projects
None yet
Development

No branches or pull requests

1 participant