Theme the credential prompts¶
The package captures interactive input — pick a mode, name a variable, enter a
secret — through the Prompter
interface. The stdlib DefaultPrompter
works everywhere, but its plain stdio prompts will not match a tool that has its own
themed terminal UI. This guide shows the two ways to make the credential prompts look
like the rest of your tool.
The seam¶
type Prompter interface {
SelectMode(ctx context.Context, title string, choices []ModeChoice) (Mode, error)
InputEnvVarName(ctx context.Context, title, placeholder string, validate func(string) error) (string, error)
InputSecret(ctx context.Context, title string) (string, error) // never echoes
}
Anywhere the package (or your setup flow) needs input, it calls these three methods. Supply your own implementation and the prompts render in your UI.
Option A — drive your existing forms off the plain data¶
If your tool already builds forms (with huh,
survey, or your own widgets), you may not need to implement Prompter at all. The
package exposes the data your forms need as plain values:
ModeChoices→ the selectable[]ModeChoice(each aMode+ label), filtered by CI and keychain availability.ValidateEnvVarName→ afunc(string) erroryou can hand straight to a form field's validator.AvailableModes,IsCI,Probe.
import "charm.land/huh/v2"
choices := credentials.ModeChoices(credentials.IsCI(), credentials.Probe(ctx),
"Environment variable reference", "OS keychain", "Literal value")
opts := make([]huh.Option[credentials.Mode], len(choices))
for i, c := range choices {
opts[i] = huh.NewOption(c.Label, c.Mode)
}
var mode credentials.Mode
form := huh.NewForm(huh.NewGroup(
huh.NewSelect[credentials.Mode]().Title("Credential storage").Options(opts...).Value(&mode),
))
The package stays free of any TUI dependency; your tool owns the widgets. This is how go-tool-base itself renders the credential prompts.
Option B — implement Prompter once, reuse it everywhere¶
If you want a single themed implementation that any credential flow can call, wrap
your UI in a Prompter:
type huhPrompter struct{}
func (huhPrompter) SelectMode(ctx context.Context, title string, choices []credentials.ModeChoice) (credentials.Mode, error) {
opts := make([]huh.Option[credentials.Mode], len(choices))
for i, c := range choices {
opts[i] = huh.NewOption(c.Label, c.Mode)
}
var mode credentials.Mode
err := huh.NewForm(huh.NewGroup(
huh.NewSelect[credentials.Mode]().Title(title).Options(opts...).Value(&mode),
)).RunWithContext(ctx)
return mode, err
}
func (huhPrompter) InputEnvVarName(ctx context.Context, title, placeholder string, validate func(string) error) (string, error) {
var name string
err := huh.NewForm(huh.NewGroup(
huh.NewInput().Title(title).Placeholder(placeholder).Validate(validate).Value(&name),
)).RunWithContext(ctx)
return name, err
}
func (huhPrompter) InputSecret(ctx context.Context, title string) (string, error) {
var secret string
err := huh.NewForm(huh.NewGroup(
huh.NewInput().Title(title).EchoMode(huh.EchoModePassword).Value(&secret),
)).RunWithContext(ctx)
return secret, err
}
Pass huhPrompter{} wherever a Prompter is expected. Your implementation must
not echo the secret (use the password echo mode above) or log it.
When the stdlib default is enough¶
For a tool with no themed UI, DefaultPrompter() is the right choice — it reads
stdin, writes prompts to stderr (so they never pollute a piped stdout), and masks
secret entry via golang.org/x/term on a real terminal, falling back to a plain line
read for piped or non-interactive input.
Related¶
- Choose a storage mode
- Getting started — the
DefaultPrompterin action