Skip to content

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:

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.