huh

repository·main·Indexed 27 days ago

https://github.com/charmbracelet/huh

A Go library for building interactive, terminal-based forms and prompts. It supports various field types including Input, Text, Select, MultiSelect, Confirm, and FilePicker. Features include dynamic forms via TitleFunc and OptionsFunc, accessibility modes for screen readers, custom themes using Lip Gloss, and integration as a tea.Model for Bubble Tea applications.

Tokens
12.3K
Snippets
20
Records
102
Agent score
93%

What's inside huh

  1. Enable Accessible Mode for screen readers

    main

    To provide a better experience for visually impaired users, you can enable an accessible mode that replaces the TUI with standard prompts. This is done using form.WithAccessible(true). It is recommended to control this via an environment variable.

    accessibleMode := os.Getenv("ACCESSIBLE") != ""
    form.WithAccessible(accessibleMode)
  2. Build interactive forms with huh?

    main

    Use huh? to create multi-field interactive forms in the terminal. Forms are composed of Groups (which act as pages), and each group contains Fields (like Select, Input, or Confirm). You can store user answers by passing pointers to variables into the .Value() method of each field. To execute the form, call form.Run().

    package main
    
    import (
        "fmt"
        "log"
        "errors"
        "charm.land/huh/v2"
    )
    
    var (
        burger       string
        toppings     []string
        sauceLevel   int
        name         string
        instructions string
        discount     bool
    )
    
    func main() {
        form := huh.NewForm(
            huh.NewGroup(
                huh.NewSelect[string]().
                    Title("Choose your burger").
                    Options(
                        huh.NewOption("Charmburger Classic", "classic"),
                        huh.NewOption("Chickwich", "chickwich"),
                    ).
                    Value(&burger),
    
                huh.NewMultiSelect[string]().
                    Title("Toppings").
                    Options(
                        huh.NewOption("Lettuce", "lettuce").Selected(true),
                        huh.NewOption("Cheese", "cheese"),
                    ).
                    Limit(4).
                    Value(&toppings),
    
                huh.NewSelect[int]().
                    Title("How much Charm Sauce?").
                    Options(
                        huh.NewOption("None", 0),
                        huh.NewOption("A little", 1),
                    ).
                    Value(&sauceLevel),
            ),
            huh.NewGroup(
                huh.NewInput().
                    Title("What’s your name?").
                    Value(&name).
                    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),
            ),
        )
    
        err := form.Run()
        if err != nil {
            log.Fatal(err)
        }
    
        fmt.Printf("Order: %s with %v sauce level\n", burger, sauceLevel)
    }
  3. Integrate Huh? into Bubble Tea applications

    main

    huh.Form implements the tea.Model interface, allowing it to be embedded directly into Bubble Tea applications. This is useful for adding form-like input to existing Bubble Tea programs or when you need more flexibility than standalone huh? provides.

    To integrate a form:

    1. Include a pointer to huh.Form in your application's Model struct.
    2. Initialize the form using huh.NewForm() and huh.NewGroup() in your constructor.
    3. In your Init() method, return m.form.Init().
    4. In your Update() method, call m.form.Update(msg) and update your model with the returned form instance.
    5. In your View() method, check m.form.State to determine if the form is completed (huh.StateCompleted) or if you should return m.form.View().
    6. Use m.form.GetString(key), m.form.GetInt(key), etc., to retrieve values once the state is completed.
    type Model struct {
        form *huh.Form // huh.Form is just a tea.Model
    }
    
    func NewModel() Model {
        return Model{
            form: huh.NewForm(
                huh.NewGroup(
                    huh.NewSelect[string]().
                        Key("class").
                        Options(huh.NewOptions("Warrior", "Mage", "Rogue")...).
                        Title("Choose your class"),
    
                huh.NewSelect[int]().
                    Key("level").
                    Options(huh.NewOptions(1, 20, 9999)...).
                    Title("Choose your level"),
                ),
            )
        }
    }
    
    func (m Model) Init() tea.Cmd {
        return m.form.Init()
    }
    
    func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
        // ...
    
        form, cmd := m.form.Update(msg)
        if f, ok := form.(*huh.Form); ok {
            m.form = f
        }
    
        return m, cmd
    }
    
    func (m Model) View() string {
        if m.form.State == huh.StateCompleted {
            class := m.form.GetString("class")
            level := m.form.GetInt("level")
            return fmt.Sprintf("You selected: %s, Lvl. %d", class, level)
        }
        return m.form.View()
    }
  4. Create Dynamic Forms

    main

    You can create forms where fields change based on previous answers by using TitleFunc and OptionsFunc instead of Title and Options.

    These functions take a func() T and a binding any. The binding tells huh? which variable to watch; whenever that variable changes, the function is recomputed. This is useful for cascading selects (e.g., selecting a Country then showing only relevant States).

    var country string
    var state string
    
    // The state field recomputes its title and options whenever 'country' changes
    huh.NewSelect[string]().
        Value(&state).
        TitleFunc(func() string {
            switch country {
            case "United States":
                return "State"
            case "Canada":
                return "Province"
            default:
                return "Territory"
            }
        }, &country).
        OptionsFunc(func() []huh.Option[string] {
            // fetchStatesForCountry is a user-defined function
            opts := fetchStatesForCountry(country)
            return huh.NewOptions(opts...)
        }, &country)
  5. Upgrade Huh from v1 to v2

    main

    To migrate from Huh v1 to v2, you must update your dependencies and change your import paths to use the charm.land vanity domain with a /v2 suffix.

    1. Update Dependencies

    Run the following commands to update your go.mod:

    go get charm.land/huh/v2@latest
    go get charm.land/bubbletea/v2@latest
    go get charm.land/lipgloss/v2@latest
    go get charm.land/bubbles/v2@latest

    2. Update Import Paths

    Replace all github.com/charmbracelet/ imports with their charm.land/.../v2 equivalents.

    v1 Importv2 Import
    github.com/charmbracelet/huhcharm.land/huh/v2
    github.com/charmbracelet/huh/spinnercharm.land/huh/v2/spinner
    github.com/charmbracelet/bubbleteacharm.land/bubbletea/v2
    github.com/charmbracelet/lipglosscharm.land/lipgloss/v2
    github.com/charmbracelet/bubblescharm.land/bubbles/v2

    3. Migration Checklist

    • Update go.mod dependencies to v2
    • Update all import paths to charm.land/ with /v2 suffix
    • Update theme calls to pass isDark bool parameter
    • Remove field-level WithAccessible() calls
    • Keep form-level WithAccessible() calls
    • Remove imports from github.com/charmbracelet/huh/accessibility package
    • Update custom themes to ThemeFunc signature
    • Run go mod tidy and tests
  6. Use FilePicker to select files

    main
    The FilePicker is a form field that allows users to browse and select files or directories from their filesystem. It supports features like showing hidden files, filtering by allowed file types, and custom validation. When a user selects a file, the path is stored in the provided accessor.
  7. Use Confirm for Yes/No prompts

    main
    The Confirm field prompts the user to confirm an action with a Yes or No choice. You can customize the labels for affirmative and negative responses using .Affirmative(string) and .Negative(string).
  8. Use Input for single-line text

    main

    The Input field prompts the user for a single line of text. You can customize the prompt character using .Prompt(), add validation logic with .Validate(), and bind the result to a variable using .Value().

    // As part of a form
    huh.NewInput().
        Title("What’s for lunch?").
        Prompt("?").
        Validate(isFood).
        Value(&lunch)
    
    // Or as a standalone blocking prompt
    var name string
    err := huh.NewInput().
        Title("What’s your name?").
        Value(&name).
        Run()
  9. Use Select for single-option selection

    main
    The Select field allows users to pick exactly one option from a list. It is generic, meaning you can store the selected value as any type (e.g., string, int). Use .Options() to provide a list of huh.NewOption items.
  10. Use View Hooks in v2

    main

    Huh v2 introduces WithViewHook, allowing you to modify the tea.View before it is rendered. This is useful for modifying view properties like AltScreen or MouseMode.

    form.WithViewHook(func(v tea.View) tea.View {
        v.AltScreen = true
        return v
    })