交互输入

CLI 工具经常需要引导用户输入配置(数据库连接信息、密钥等)。直接用 fmt.Scan 体验很差,huh 是 Charm 生态下的表单库,可以用少量代码构建出带标题、占位符、密码掩码和帮助提示的终端交互表单。

安装:

go get github.com/charmbracelet/huh

核心概念:

  • huh.NewInput() 创建一个输入框,Title 设置提示标题,Value(&v) 把用户输入绑定到变量
  • EchoMode(huh.EchoModePassword) 让输入以 * 掩码显示,适合密码字段
  • huh.NewForm(huh.NewGroup(inp)).Run() 运行表单,一次可以包含多个输入项
  • Ctrl+C 取消时 Run 返回 err,上层据此优雅退出

下面的示例来自 onboard 初始化命令:引导用户输入 PostgreSQL 连接信息(直接回车采用默认值),最后拼装成 DSN 连接串。promptString 的逻辑是「空输入返回默认值」,promptPassword 则使用密码掩码模式:

package main

import (
	"fmt"
	"net/url"
	"os"

	"github.com/charmbracelet/huh"
)

func main() {
	fmt.Println("── Database Connection ──")
	fmt.Println("  Enter your PostgreSQL connection details (press Enter for defaults).")
	fmt.Println()

	dsn, err := promptPostgresFields()
	if err != nil {
		fmt.Println("Cancelled.")
		os.Exit(1)
	}

	fmt.Println()
	fmt.Println("  Assembled DSN:")
	fmt.Printf("    %s\n", dsn)
}

// promptPostgresFields 对应 goclaw/cmd/onboard_helpers.go。
func promptPostgresFields() (string, error) {
	host, err := promptString("Host", "", "localhost")
	if err != nil {
		return "", err
	}
	port, err := promptString("Port", "", "5432")
	if err != nil {
		return "", err
	}
	dbName, err := promptString("Database name", "", "goclaw")
	if err != nil {
		return "", err
	}
	user, err := promptString("Username", "", "postgres")
	if err != nil {
		return "", err
	}
	password, err := promptPassword("Password", "Leave empty if no password")
	if err != nil {
		return "", err
	}
	sslMode, err := promptString("SSL mode", "", "disable")
	if err != nil {
		return "", err
	}

	var userInfo *url.Userinfo
	if password != "" {
		userInfo = url.UserPassword(user, password)
	} else {
		userInfo = url.User(user)
	}
	dsn := fmt.Sprintf("postgres://%s@%s:%s/%s?sslmode=%s", userInfo.String(), host, port, dbName, sslMode)
	return dsn, nil
}

// promptString:空输入(直接 Enter)返回 defaultVal。对照 goclaw/cmd/prompt.go。
func promptString(title, description, defaultVal string) (string, error) {
	var value string
	inp := huh.NewInput().
		Title(title).
		Value(&value)

	if description != "" {
		inp = inp.Description(description)
	}
	if defaultVal != "" {
		inp = inp.Placeholder(defaultVal)
	}

	if err := huh.NewForm(huh.NewGroup(inp)).WithShowHelp(true).Run(); err != nil {
		return "", err
	}
	if value == "" {
		return defaultVal, nil
	}
	return value, nil
}

func promptPassword(title, description string) (string, error) {
	var value string
	inp := huh.NewInput().
		Title(title).
		EchoMode(huh.EchoModePassword).
		Value(&value)

	if description != "" {
		inp = inp.Description(description)
	}

	if err := huh.NewForm(huh.NewGroup(inp)).WithShowHelp(true).Run(); err != nil {
		return "", err
	}
	return value, nil
}