charm-huh
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinesecharmbracelet/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/v2Quick 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
架构
FormGroupFieldGroups are displayed one at a time. The form advances to the next group when all fields in the current group pass validation.
FormGroupField分组会逐个显示,当前分组的所有字段通过验证后,表单才会进入下一个分组。
Form
表单(Form)
go
form := huh.NewForm(groups...*Group) *FormKey methods:
| Method | Purpose |
|---|---|
| Block and run the form |
| Run with context (supports cancellation) |
| Set theme |
| Set dimensions |
| Screen reader mode |
| Toggle help bar |
| Toggle error display |
| Auto-cancel after duration |
| Set group layout |
| Custom keybindings |
| Custom IO |
Retrieve values by key after completion:
go
form.GetString("key")
form.GetInt("key")
form.GetBool("key")
form.Get("key") // anyForm states: , ,
huh.StateNormalhuh.StateCompletedhuh.StateAbortedErrors: (ctrl+c),
huh.ErrUserAbortedhuh.ErrTimeoutgo
form := huh.NewForm(groups...*Group) *Form关键方法:
| 方法 | 用途 |
|---|---|
| 阻塞并运行表单 |
| 携带上下文运行(支持取消) |
| 设置主题 |
| 设置尺寸 |
| 屏幕阅读器模式 |
| 切换帮助栏显示 |
| 切换错误信息显示 |
| 超时后自动取消 |
| 设置分组布局 |
| 自定义按键绑定 |
| 自定义输入输出 |
完成后通过键名获取值:
go
form.GetString("key")
form.GetInt("key")
form.GetBool("key")
form.Get("key") // 获取任意类型表单状态:、、
huh.StateNormalhuh.StateCompletedhuh.StateAborted错误类型:(按下ctrl+c)、
huh.ErrUserAbortedhuh.ErrTimeoutGroup
分组(Group)
go
group := huh.NewGroup(fields ...Field) *Group| Method | Purpose |
|---|---|
| Group header |
| Skip this group |
| Conditionally skip group |
| Toggle help for group |
go
group := huh.NewGroup(fields ...Field) *Group| 方法 | 用途 |
|---|---|
| 设置分组标题/描述 |
| 跳过当前分组 |
| 条件性跳过分组 |
| 切换分组帮助显示 |
Field Types
字段类型
Every field supports: , , , , , .
.Title(s).Description(s).Key(s).Value(&v).Validate(fn).Run()Dynamic variants exist for most properties: , , etc.
.TitleFunc(fn, binding).DescriptionFunc(fn, binding)所有字段均支持:、、、、、。
.Title(s).Description(s).Key(s).Value(&v).Validate(fn).Run()大部分属性支持动态变体:、等。
.TitleFunc(fn, binding).DescriptionFunc(fn, binding)Input
输入框(Input)
Single line text. Type: .
stringgo
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)单行文本输入,类型:。
stringgo
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: .
stringgo
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)多行文本输入,类型:。
stringgo
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, to select all (when no limit), to filter.
a/选择零项或多项,泛型:。
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: .
boolgo
huh.NewConfirm().
Title("Continue?").
Affirmative("Yes!").
Negative("No way").
Inline(true).
Value(&ok)Keys: / or left/right to toggle, to accept, to reject.
hlyn是/否选择,类型:。
boolgo
huh.NewConfirm().
Title("Continue?").
Affirmative("Yes!").
Negative("No way").
Inline(true).
Value(&ok)快捷键:/或左右箭头切换,确认,取消。
hlynNote
提示框(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 variants to recompute properties when bindings change. Pass a pointer to the bound variable.
*Funcgo
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 () tells huh when to recompute. Results are cached per binding hash.
&country使用变体,在绑定变量变化时重新计算属性。传入绑定变量的指针。
*Funcgo
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)绑定参数()用于通知huh何时重新计算,结果会根据绑定哈希缓存。
&countryValidation
验证
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: (default), , , , .
ThemeCharmThemeDraculaThemeCatppuccinThemeBase16ThemeDefaultgo
form.WithTheme(huh.ThemeFunc(huh.ThemeDracula))Custom theme - implement the interface:
Themego
type Theme interface {
Theme(isDark bool) *Styles
}Or use :
ThemeFuncgo
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
}))内置主题:(默认)、、、、。
ThemeCharmThemeDraculaThemeCatppuccinThemeBase16ThemeDefaultgo
form.WithTheme(huh.ThemeFunc(huh.ThemeDracula))自定义主题——实现接口:
Themego
type Theme interface {
Theme(isDark bool) *Styles
}或使用:
ThemeFuncgo
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 columnsgo
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 , accessible mode activates automatically. Replaces TUI with plain text prompts. Timeout is not supported in accessible mode.
TERM=dumbgo
accessible := os.Getenv("ACCESSIBLE") != ""
form.WithAccessible(accessible)当时,会自动激活无障碍模式,将TUI替换为纯文本提示框。无障碍模式不支持超时功能。
TERM=dumbSpinner
加载动画(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 on a form or individual field. Blocks until complete.
.Run()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.Formtea.Modelgo
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 , use Init/Update/View cycle instead
.Run() - Set and
SubmitCmdif you want custom behavior on form completionCancelCmd - Use on fields, retrieve with
.Key("name")form.GetString("name") - Check to know when the form is done
form.State - 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.Formtea.Modelgo
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()
}嵌入时的关键差异:
- 不要调用,而是使用Init/Update/View循环
.Run() - 若需在表单完成时自定义行为,设置和
SubmitCmdCancelCmd - 为字段设置,通过
.Key("name")获取值form.GetString("name") - 检查判断表单是否完成
form.State - 对Update结果进行类型断言:
form.(*huh.Form)
支持程序化控制的导航方法:
- 、
form.NextGroup()form.PrevGroup() - 、
form.NextField()form.PrevField() form.GetFocusedField()
Common Mistakes
常见错误
-
Forgetting- Without it, answers go nowhere. The field uses an internal
.Value(&v)that you cannot read after form completes unless you useEmbeddedAccessor+.Key().form.GetString() -
Usinginside Bubble Tea - Never call
.Run()on an embedded form. Use the Init/Update/View pattern..Run() -
Missing type parameter on Select/MultiSelect -not
huh.NewSelect[string](). The generic parameter determines the option value type.huh.NewSelect() -
Dynamic binding without pointer -will not work. Must be
TitleFunc(fn, country)with a pointer so huh can detect changes.TitleFunc(fn, &country) -
Timeout in accessible mode -returns
WithTimeout()in accessible mode. Guard it.ErrTimeoutUnsupported -
Not handling ErrUserAborted -returns
form.Run()when user presses ctrl+c. Always check the error.huh.ErrUserAborted -
Form outputs to stderr - By default, the TUI renders to stderr (stdout stays clean for piping). Useto change.
.WithOutput(os.Stdout)
-
忘记设置- 不设置的话,输入内容无法存储。除非使用
.Value(&v)+.Key(),否则表单完成后无法读取字段使用的内部form.GetString()。EmbeddedAccessor -
在Bubble Tea中调用- 嵌入表单时绝不要调用
.Run(),请使用Init/Update/View模式。.Run() -
Select/MultiSelect缺少类型参数 - 应使用而非
huh.NewSelect[string](),泛型参数决定选项值的类型。huh.NewSelect() -
动态绑定未传入指针 -无法生效,必须传入指针
TitleFunc(fn, country),这样huh才能检测到变化。TitleFunc(fn, &country) -
无障碍模式下使用超时 - 无障碍模式下会返回
WithTimeout(),需提前处理。ErrTimeoutUnsupported -
未处理ErrUserAborted - 用户按下ctrl+c时会返回
form.Run(),务必检查错误。huh.ErrUserAborted -
表单输出到stderr - 默认情况下,TUI会渲染到stderr(保持stdout干净以便管道输出),可使用修改输出目标。
.WithOutput(os.Stdout)
Checklist
检查清单
- Import
charm.land/huh/v2 - Every field that stores data has or
.Value(&var).Key("name") - Custom validators return on success,
nilon failureerror - Dynamic fields use variants with pointer bindings
*Func - Accessible mode handled via env var or config flag
- handled after
ErrUserAborted.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") - 自定义验证器在成功时返回,失败时返回
nilerror - 动态字段使用变体并传入指针绑定
*Func - 通过环境变量或配置标志处理无障碍模式
- 后处理
.Run()错误ErrUserAborted - 嵌入表单使用Init/Update/View模式,而非
.Run() - 通过检查表单状态
form.State == huh.StateCompleted