charm-huh

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

charmbracelet/huh

charmbracelet/huh

Interactive terminal forms and prompts for Go. Built on Bubble Tea.
Import:
charm.land/huh/v2
适用于 Go 的交互式终端表单与提示框库,基于 Bubble Tea 构建。
导入:
charm.land/huh/v2

Quick Start

快速开始

go
package main

import (
    "fmt"
    "log"

    "charm.land/huh/v2"
)

func main() {
    var name string
    var confirm bool

    err := huh.NewForm(
        huh.NewGroup(
            huh.NewInput().
                Title("What's your name?").
                Value(&name).
                Validate(huh.ValidateNotEmpty()),
            huh.NewConfirm().
                Title("Ready?").
                Value(&confirm),
        ),
    ).Run()
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Hello, %s!\n", name)
}
Single field shorthand (no Form/Group wrapper needed):
go
var name string
huh.NewInput().Title("Name?").Value(&name).Run()
go
package main

import (
    "fmt"
    "log"

    "charm.land/huh/v2"
)

func main() {
    var name string
    var confirm bool

    err := huh.NewForm(
        huh.NewGroup(
            huh.NewInput().
                Title("What's your name?").
                Value(&name).
                Validate(huh.ValidateNotEmpty()),
            huh.NewConfirm().
                Title("Ready?").
                Value(&confirm),
        ),
    ).Run()
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Hello, %s!\n", name)
}
单字段简写(无需 Form/Group 包装):
go
var name string
huh.NewInput().Title("Name?").Value(&name).Run()

Core API

核心 API

Architecture

架构

Form
>
Group
(pages) >
Field
(inputs)
Groups are displayed one at a time. The form advances to the next group when all fields in the current group pass validation.
Form
>
Group
(页面)>
Field
(输入项)
分组会逐个显示,当前分组的所有字段通过验证后,表单才会进入下一个分组。

Form

表单(Form)

go
form := huh.NewForm(groups...*Group) *Form
Key methods:
MethodPurpose
.Run()
Block and run the form
.RunWithContext(ctx)
Run with context (supports cancellation)
.WithTheme(theme)
Set theme
.WithWidth(w)
/
.WithHeight(h)
Set dimensions
.WithAccessible(bool)
Screen reader mode
.WithShowHelp(bool)
Toggle help bar
.WithShowErrors(bool)
Toggle error display
.WithTimeout(duration)
Auto-cancel after duration
.WithLayout(layout)
Set group layout
.WithKeyMap(keymap)
Custom keybindings
.WithOutput(w)
/
.WithInput(r)
Custom IO
Retrieve values by key after completion:
go
form.GetString("key")
form.GetInt("key")
form.GetBool("key")
form.Get("key") // any
Form states:
huh.StateNormal
,
huh.StateCompleted
,
huh.StateAborted
Errors:
huh.ErrUserAborted
(ctrl+c),
huh.ErrTimeout
go
form := huh.NewForm(groups...*Group) *Form
关键方法:
方法用途
.Run()
阻塞并运行表单
.RunWithContext(ctx)
携带上下文运行(支持取消)
.WithTheme(theme)
设置主题
.WithWidth(w)
/
.WithHeight(h)
设置尺寸
.WithAccessible(bool)
屏幕阅读器模式
.WithShowHelp(bool)
切换帮助栏显示
.WithShowErrors(bool)
切换错误信息显示
.WithTimeout(duration)
超时后自动取消
.WithLayout(layout)
设置分组布局
.WithKeyMap(keymap)
自定义按键绑定
.WithOutput(w)
/
.WithInput(r)
自定义输入输出
完成后通过键名获取值:
go
form.GetString("key")
form.GetInt("key")
form.GetBool("key")
form.Get("key") // 获取任意类型
表单状态:
huh.StateNormal
huh.StateCompleted
huh.StateAborted
错误类型:
huh.ErrUserAborted
(按下ctrl+c)、
huh.ErrTimeout

Group

分组(Group)

go
group := huh.NewGroup(fields ...Field) *Group
MethodPurpose
.Title(s)
/
.Description(s)
Group header
.WithHide(bool)
Skip this group
.WithHideFunc(func() bool)
Conditionally skip group
.WithShowHelp(bool)
Toggle help for group
go
group := huh.NewGroup(fields ...Field) *Group
方法用途
.Title(s)
/
.Description(s)
设置分组标题/描述
.WithHide(bool)
跳过当前分组
.WithHideFunc(func() bool)
条件性跳过分组
.WithShowHelp(bool)
切换分组帮助显示

Field Types

字段类型

Every field supports:
.Title(s)
,
.Description(s)
,
.Key(s)
,
.Value(&v)
,
.Validate(fn)
,
.Run()
.
Dynamic variants exist for most properties:
.TitleFunc(fn, binding)
,
.DescriptionFunc(fn, binding)
, etc.
所有字段均支持:
.Title(s)
.Description(s)
.Key(s)
.Value(&v)
.Validate(fn)
.Run()
大部分属性支持动态变体:
.TitleFunc(fn, binding)
.DescriptionFunc(fn, binding)
等。

Input

输入框(Input)

Single line text. Type:
string
.
go
huh.NewInput().
    Title("Email").
    Placeholder("you@example.com").
    Prompt("> ").
    CharLimit(100).
    Suggestions([]string{"gmail.com", "outlook.com"}).
    EchoMode(huh.EchoModePassword). // or EchoModeNone
    Inline(true). // title and input on same line
    Validate(huh.ValidateNotEmpty()).
    Value(&email)
单行文本输入,类型:
string
go
huh.NewInput().
    Title("Email").
    Placeholder("you@example.com").
    Prompt("> ").
    CharLimit(100).
    Suggestions([]string{"gmail.com", "outlook.com"}).
    EchoMode(huh.EchoModePassword). // 或 EchoModeNone
    Inline(true). // 标题与输入框同行显示
    Validate(huh.ValidateNotEmpty()).
    Value(&email)

Text

文本域(Text)

Multi-line textarea. Type:
string
.
go
huh.NewText().
    Title("Description").
    Lines(5).
    CharLimit(500).
    Placeholder("Enter details...").
    ShowLineNumbers(true).
    Editor("vim"). // external editor support (ctrl+e)
    EditorExtension("md").
    ExternalEditor(false). // disable external editor
    Value(&description)
多行文本输入,类型:
string
go
huh.NewText().
    Title("Description").
    Lines(5).
    CharLimit(500).
    Placeholder("Enter details...").
    ShowLineNumbers(true).
    Editor("vim"). // 支持外部编辑器(ctrl+e)
    EditorExtension("md").
    ExternalEditor(false). // 禁用外部编辑器
    Value(&description)

Select

单选框(Select)

Pick one from a list. Generic:
Select[T comparable]
.
go
huh.NewSelect[string]().
    Title("Country").
    Options(
        huh.NewOption("United States", "US"),
        huh.NewOption("Canada", "CA"),
    ).
    Height(8).      // scrollable if options exceed height
    Inline(true).   // horizontal left/right navigation
    Filtering(true). // start with filter active
    Value(&country)
Shorthand for simple options:
go
Options(huh.NewOptions("Warrior", "Mage", "Rogue")...)
从列表中选择一项,泛型:
Select[T comparable]
go
huh.NewSelect[string]().
    Title("Country").
    Options(
        huh.NewOption("United States", "US"),
        huh.NewOption("Canada", "CA"),
    ).
    Height(8).      // 选项超出高度时可滚动
    Inline(true).   // 横向左右导航
    Filtering(true). // 默认启用筛选
    Value(&country)
简单选项的简写:
go
Options(huh.NewOptions("Warrior", "Mage", "Rogue")...)

MultiSelect

多选框(MultiSelect)

Pick zero or more. Generic:
MultiSelect[T comparable]
.
go
huh.NewMultiSelect[string]().
    Title("Toppings").
    Options(
        huh.NewOption("Lettuce", "lettuce").Selected(true),
        huh.NewOption("Tomato", "tomato"),
        huh.NewOption("Cheese", "cheese"),
    ).
    Limit(3).
    Height(6).
    Filterable(false). // disable "/" filter
    Value(&toppings)
Space to toggle,
a
to select all (when no limit),
/
to filter.
选择零项或多项,泛型:
MultiSelect[T comparable]
go
huh.NewMultiSelect[string]().
    Title("Toppings").
    Options(
        huh.NewOption("Lettuce", "lettuce").Selected(true),
        huh.NewOption("Tomato", "tomato"),
        huh.NewOption("Cheese", "cheese"),
    ).
    Limit(3).
    Height(6).
    Filterable(false). // 禁用 "/" 筛选
    Value(&toppings)
按空格切换选择,
a
键全选(无数量限制时),
/
键触发筛选。

Confirm

确认框(Confirm)

Yes/No. Type:
bool
.
go
huh.NewConfirm().
    Title("Continue?").
    Affirmative("Yes!").
    Negative("No way").
    Inline(true).
    Value(&ok)
Keys:
h
/
l
or left/right to toggle,
y
to accept,
n
to reject.
是/否选择,类型:
bool
go
huh.NewConfirm().
    Title("Continue?").
    Affirmative("Yes!").
    Negative("No way").
    Inline(true).
    Value(&ok)
快捷键:
h
/
l
或左右箭头切换,
y
确认,
n
取消。

Note

提示框(Note)

Display-only. Not interactive by default (auto-skipped).
go
huh.NewNote().
    Title("Welcome").
    Description("This form collects your _preferences_.").
    Height(10).
    Next(true).        // show a "Next" button, makes it interactive
    NextLabel("Continue")
Description supports basic markdown:
_italic_
,
*bold*
,
`code`
.
仅用于展示,默认非交互式(自动跳过)。
go
huh.NewNote().
    Title("Welcome").
    Description("This form collects your _preferences_.").
    Height(10).
    Next(true).        // 显示“下一步”按钮,变为交互式
    NextLabel("Continue")
描述支持基础Markdown语法:
_斜体_
*粗体*
`代码`

Common Patterns

常见模式

Multi-Step Forms

多步骤表单

Groups act as pages. Users navigate forward/back between them.
go
huh.NewForm(
    huh.NewGroup(/* step 1 fields */).Title("Step 1"),
    huh.NewGroup(/* step 2 fields */).Title("Step 2"),
    huh.NewGroup(/* step 3 fields */).Title("Step 3"),
).Run()
分组作为页面,用户可在分组间前后导航。
go
huh.NewForm(
    huh.NewGroup(/* 第一步字段 */).Title("Step 1"),
    huh.NewGroup(/* 第二步字段 */).Title("Step 2"),
    huh.NewGroup(/* 第三步字段 */).Title("Step 3"),
).Run()

Conditional Groups

条件性分组

Hide groups based on previous answers:
go
var wantExtras bool

huh.NewForm(
    huh.NewGroup(
        huh.NewConfirm().Title("Want extras?").Value(&wantExtras),
    ),
    huh.NewGroup(
        huh.NewInput().Title("Extra details").Value(&details),
    ).WithHideFunc(func() bool { return !wantExtras }),
).Run()
根据之前的答案隐藏分组:
go
var wantExtras bool

huh.NewForm(
    huh.NewGroup(
        huh.NewConfirm().Title("Want extras?").Value(&wantExtras),
    ),
    huh.NewGroup(
        huh.NewInput().Title("Extra details").Value(&details),
    ).WithHideFunc(func() bool { return !wantExtras }),
).Run()

Dynamic Fields

动态字段

Use
*Func
variants to recompute properties when bindings change. Pass a pointer to the bound variable.
go
var country string

huh.NewSelect[string]().
    Value(&state).
    TitleFunc(func() string {
        if country == "Canada" { return "Province" }
        return "State"
    }, &country).
    OptionsFunc(func() []huh.Option[string] {
        return huh.NewOptions(statesByCountry[country]...)
    }, &country)
The binding (
&country
) tells huh when to recompute. Results are cached per binding hash.
使用
*Func
变体,在绑定变量变化时重新计算属性。传入绑定变量的指针。
go
var country string

huh.NewSelect[string]().
    Value(&state).
    TitleFunc(func() string {
        if country == "Canada" { return "Province" }
        return "State"
    }, &country).
    OptionsFunc(func() []huh.Option[string] {
        return huh.NewOptions(statesByCountry[country]...)
    }, &country)
绑定参数(
&country
)用于通知huh何时重新计算,结果会根据绑定哈希缓存。

Validation

验证

Built-in validators:
go
huh.ValidateNotEmpty()
huh.ValidateMinLength(3)
huh.ValidateMaxLength(100)
huh.ValidateLength(3, 100)  // min and max
huh.ValidateOneOf("a", "b", "c")
Custom validation:
go
.Validate(func(s string) error {
    if !strings.Contains(s, "@") {
        return fmt.Errorf("must be a valid email")
    }
    return nil
})
Validation runs on blur (when leaving a field). Forms block progression if any field in the current group has errors.
内置验证器:
go
huh.ValidateNotEmpty()
huh.ValidateMinLength(3)
huh.ValidateMaxLength(100)
huh.ValidateLength(3, 100)  // 最小与最大长度
 huh.ValidateOneOf("a", "b", "c")
自定义验证:
go
.Validate(func(s string) error {
    if !strings.Contains(s, "@") {
        return fmt.Errorf("must be a valid email")
    }
    return nil
})
验证在字段失焦时触发,若当前分组存在错误字段,表单会阻止进入下一分组。

Theming

主题定制

Built-in themes:
ThemeCharm
(default),
ThemeDracula
,
ThemeCatppuccin
,
ThemeBase16
,
ThemeDefault
.
go
form.WithTheme(huh.ThemeFunc(huh.ThemeDracula))
Custom theme - implement the
Theme
interface:
go
type Theme interface {
    Theme(isDark bool) *Styles
}
Or use
ThemeFunc
:
go
form.WithTheme(huh.ThemeFunc(func(isDark bool) *huh.Styles {
    s := huh.ThemeCharm(isDark) // start from a base
    s.Focused.Title = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("212"))
    return s
}))
内置主题:
ThemeCharm
(默认)、
ThemeDracula
ThemeCatppuccin
ThemeBase16
ThemeDefault
go
form.WithTheme(huh.ThemeFunc(huh.ThemeDracula))
自定义主题——实现
Theme
接口:
go
type Theme interface {
    Theme(isDark bool) *Styles
}
或使用
ThemeFunc
go
form.WithTheme(huh.ThemeFunc(func(isDark bool) *huh.Styles {
    s := huh.ThemeCharm(isDark) // 基于基础主题修改
    s.Focused.Title = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("212"))
    return s
}))

Layouts

布局

go
form.WithLayout(huh.LayoutDefault)         // one group at a time (default)
form.WithLayout(huh.LayoutStack)           // all groups stacked vertically
form.WithLayout(huh.LayoutColumns(2))      // groups in 2 columns
form.WithLayout(huh.LayoutGrid(2, 3))      // 2 rows, 3 columns
go
form.WithLayout(huh.LayoutDefault)         // 一次显示一个分组(默认)
form.WithLayout(huh.LayoutStack)           // 所有分组垂直堆叠
form.WithLayout(huh.LayoutColumns(2))      // 分组分为2列
form.WithLayout(huh.LayoutGrid(2, 3))      // 2行3列网格布局

Accessibility

无障碍支持

go
accessible := os.Getenv("ACCESSIBLE") != ""
form.WithAccessible(accessible)
When
TERM=dumb
, accessible mode activates automatically. Replaces TUI with plain text prompts. Timeout is not supported in accessible mode.
go
accessible := os.Getenv("ACCESSIBLE") != ""
form.WithAccessible(accessible)
TERM=dumb
时,会自动激活无障碍模式,将TUI替换为纯文本提示框。无障碍模式不支持超时功能。

Spinner

加载动画(Spinner)

Separate package for loading indicators after form submission:
go
import "charm.land/huh/v2/spinner"

err := spinner.New().
    Title("Processing...").
    Action(func() { /* do work */ }).
    Run()
Or with context:
go
go doWork()
spinner.New().Title("Working...").Context(ctx).Run()
表单提交后用于显示加载状态的独立包:
go
import "charm.land/huh/v2/spinner"

err := spinner.New().
    Title("Processing...").
    Action(func() { /* 执行任务 */ }).
    Run()
或携带上下文:
go
go doWork()
spinner.New().Title("Working...").Context(ctx).Run()

Integration: Standalone vs Bubble Tea

集成方式:独立使用 vs Bubble Tea

Standalone

独立使用

Call
.Run()
on a form or individual field. Blocks until complete.
go
form.Run()
// or
huh.NewInput().Title("Name?").Value(&name).Run()
调用表单或单个字段的
.Run()
方法,阻塞直到完成。
go
form.Run()
// 或
huh.NewInput().Title("Name?").Value(&name).Run()

Embedded in Bubble Tea

嵌入 Bubble Tea

*huh.Form
implements
tea.Model
. Use it as a component in your Bubble Tea app.
go
type Model struct {
    form *huh.Form
}

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"),
            ),
        ),
    }
}

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
    }

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

func (m Model) View() string {
    if m.form.State == huh.StateCompleted {
        return fmt.Sprintf("You picked: %s", m.form.GetString("class"))
    }
    return m.form.View()
}
Key differences when embedded:
  • Do NOT call
    .Run()
    , use Init/Update/View cycle instead
  • Set
    SubmitCmd
    and
    CancelCmd
    if you want custom behavior on form completion
  • Use
    .Key("name")
    on fields, retrieve with
    form.GetString("name")
  • Check
    form.State
    to know when the form is done
  • Type assert the Update result:
    form.(*huh.Form)
Navigation methods available for programmatic control:
  • form.NextGroup()
    ,
    form.PrevGroup()
  • form.NextField()
    ,
    form.PrevField()
  • form.GetFocusedField()
*huh.Form
实现了
tea.Model
,可作为组件嵌入Bubble Tea应用。
go
type Model struct {
    form *huh.Form
}

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"),
            ),
        ),
    }
}

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
    }

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

func (m Model) View() string {
    if m.form.State == huh.StateCompleted {
        return fmt.Sprintf("You picked: %s", m.form.GetString("class"))
    }
    return m.form.View()
}
嵌入时的关键差异:
  • 不要调用
    .Run()
    ,而是使用Init/Update/View循环
  • 若需在表单完成时自定义行为,设置
    SubmitCmd
    CancelCmd
  • 为字段设置
    .Key("name")
    ,通过
    form.GetString("name")
    获取值
  • 检查
    form.State
    判断表单是否完成
  • 对Update结果进行类型断言:
    form.(*huh.Form)
支持程序化控制的导航方法:
  • form.NextGroup()
    form.PrevGroup()
  • form.NextField()
    form.PrevField()
  • form.GetFocusedField()

Common Mistakes

常见错误

  1. Forgetting
    .Value(&v)
    - Without it, answers go nowhere. The field uses an internal
    EmbeddedAccessor
    that you cannot read after form completes unless you use
    .Key()
    +
    form.GetString()
    .
  2. Using
    .Run()
    inside Bubble Tea
    - Never call
    .Run()
    on an embedded form. Use the Init/Update/View pattern.
  3. Missing type parameter on Select/MultiSelect -
    huh.NewSelect[string]()
    not
    huh.NewSelect()
    . The generic parameter determines the option value type.
  4. Dynamic binding without pointer -
    TitleFunc(fn, country)
    will not work. Must be
    TitleFunc(fn, &country)
    with a pointer so huh can detect changes.
  5. Timeout in accessible mode -
    WithTimeout()
    returns
    ErrTimeoutUnsupported
    in accessible mode. Guard it.
  6. Not handling ErrUserAborted -
    form.Run()
    returns
    huh.ErrUserAborted
    when user presses ctrl+c. Always check the error.
  7. Form outputs to stderr - By default, the TUI renders to stderr (stdout stays clean for piping). Use
    .WithOutput(os.Stdout)
    to change.
  1. 忘记设置
    .Value(&v)
    - 不设置的话,输入内容无法存储。除非使用
    .Key()
    +
    form.GetString()
    ,否则表单完成后无法读取字段使用的内部
    EmbeddedAccessor
  2. 在Bubble Tea中调用
    .Run()
    - 嵌入表单时绝不要调用
    .Run()
    ,请使用Init/Update/View模式。
  3. Select/MultiSelect缺少类型参数 - 应使用
    huh.NewSelect[string]()
    而非
    huh.NewSelect()
    ,泛型参数决定选项值的类型。
  4. 动态绑定未传入指针 -
    TitleFunc(fn, country)
    无法生效,必须传入指针
    TitleFunc(fn, &country)
    ,这样huh才能检测到变化。
  5. 无障碍模式下使用超时 - 无障碍模式下
    WithTimeout()
    会返回
    ErrTimeoutUnsupported
    ,需提前处理。
  6. 未处理ErrUserAborted - 用户按下ctrl+c时
    form.Run()
    会返回
    huh.ErrUserAborted
    ,务必检查错误。
  7. 表单输出到stderr - 默认情况下,TUI会渲染到stderr(保持stdout干净以便管道输出),可使用
    .WithOutput(os.Stdout)
    修改输出目标。

Checklist

检查清单

  • Import
    charm.land/huh/v2
  • Every field that stores data has
    .Value(&var)
    or
    .Key("name")
  • Custom validators return
    nil
    on success,
    error
    on failure
  • Dynamic fields use
    *Func
    variants with pointer bindings
  • Accessible mode handled via env var or config flag
  • ErrUserAborted
    handled after
    .Run()
  • Embedded forms use Init/Update/View, not
    .Run()
  • Form state checked via
    form.State == huh.StateCompleted
  • 导入
    charm.land/huh/v2
  • 所有需要存储数据的字段都设置了
    .Value(&var)
    .Key("name")
  • 自定义验证器在成功时返回
    nil
    ,失败时返回
    error
  • 动态字段使用
    *Func
    变体并传入指针绑定
  • 通过环境变量或配置标志处理无障碍模式
  • .Run()
    后处理
    ErrUserAborted
    错误
  • 嵌入表单使用Init/Update/View模式,而非
    .Run()
  • 通过
    form.State == huh.StateCompleted
    检查表单状态