还在用 fmt.Println 写命令行工具?太枯燥啦!本文带你入门 Go 语言中最火的 TUI 框架 Bubble Tea,通过两个开箱即用的实战 Demo(交互式列表、动态进度条),让你轻松构建出现代、酷炫、交互式的命令行界面。
Github: github.com/charmbracel…
是否曾惊叹于 docker
、kubectl
或 gh
等工具那丝滑、美观的命令行界面?你是否也想为你自己的 Go 程序打造一个同样出色的终端用户界面(TUI),告别单调的黑底白字?
那么今天的主角——Bubble Tea——绝对是你的不二之选!
什么是 Bubble Tea?
Bubble Tea 是一个基于 Go 语言的 TUI(Terminal User Interface)框架,由 Charm 开发。它最大的特点是采用了 Elm 架构(一种函数式编程架构),让你可以用一种声明式、状态驱动的方式来构建复杂的终端应用。
忘掉那些繁琐地计算光标位置、处理底层终端事件的陈旧方法吧!使用 Bubble Tea,你只需要关心三件事:
- Model (模型) :你的应用长什么样?即应用在任何时刻的状态。例如,一个列表的当前选项、一个进度条的当前百分比等。
- Update (更新) :应用状态应该如何变化?根据用户的按键、窗口大小变化或其他事件来更新你的 Model。
- View (视图) :如何将你的 Model 渲染成字符串,展示给用户?
这种 Model -> View -> Update 的循环让你的代码逻辑异常清晰,易于管理和扩展。
快速安装
安装 Bubble Tea 非常简单,它遵循标准的 Go Modules 工作流。
打开你的终端,运行以下命令:
go get github.com/charmbracelet/bubbletea
好了,就是这么简单!现在,让我们泡一杯“茶”,开始我们的 TUI 之旅吧。
入门 Demo:一个简单的交互式列表
最经典的 TUI 场景莫过于一个可供选择的列表。我们将创建一个程序,它会显示一个购物清单,你可以用 ↑
和 ↓
键来移动光标,按 Enter
键选择一项,按 q
或 esc
退出。
准备工作:
- 创建一个新的项目目录,例如
bubbletea-demo
。 - 进入目录并初始化 Go Module:
go mod init bubbletea-demo
- 创建
main.go
文件,并将下面的代码粘贴进去。
完整代码 main.go
:
package main
import (
"fmt"
"os"
tea "github.com/charmbracelet/bubbletea"
)
// Model 定义了我们应用的状态
type model struct {
choices []string // 选项列表
cursor int // 光标当前在哪一项上
selected map[int]struct{} // 哪些项被选中了
}
// initialModel 初始化我们的应用状态
func initialModel() model {
return model{
choices: []string{"购买胡萝卜 🥕", "购买芹菜 🥬", "购买土豆 🥔"},
selected: make(map[int]struct{}),
}
}
// Init 是 Bubble Tea 程序启动时运行的第一个命令
func (m model) Init() tea.Cmd {
// 在这里,我们不需要在启动时执行任何异步任务,所以返回 nil
return nil
}
// Update 处理所有的事件输入,并返回更新后的 Model 和需要执行的 Command
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
// Msg 是一个 KeyMsg 类型吗?
case tea.KeyMsg:
// 是哪一个按键?
switch msg.String() {
// 'q', 'ctrl+c' 会退出程序
case "ctrl+c", "q":
return m, tea.Quit
// 'up', 'k' 向上移动光标
case "up", "k":
if m.cursor > 0 {
m.cursor--
}
// 'down', 'j' 向下移动光标
case "down", "j":
if m.cursor < len(m.choices)-1 {
m.cursor++
}
// 'enter', ' ' 选中/取消选中当前项
case "enter", " ":
_, ok := m.selected[m.cursor]
if ok {
delete(m.selected, m.cursor)
} else {
m.selected[m.cursor] = struct{}{}
}
}
}
// 返回更新后的模型给 Bubble Tea runtime
return m, nil
}
// View 将模型渲染成用户能看到的字符串
func (m model) View() string {
s := "你想买什么?\n\n"
for i, choice := range m.choices {
// 光标是否在当前项上?
cursor := " " // not selected
if m.cursor == i {
cursor = ">" // selected
}
// 当前项是否被选中?
checked := " " // not checked
if _, ok := m.selected[i]; ok {
checked = "x" // checked
}
// 渲染行
s += fmt.Sprintf("%s [%s] %s\n", cursor, checked, choice)
}
s += "\n按 'q' 或 'ctrl+c' 退出。\n"
return s
}
func main() {
p := tea.NewProgram(initialModel())
if _, err := p.Run(); err != nil {
fmt.Printf("程序运行出错: %v", err)
os.Exit(1)
}
}
运行它:
在终端中执行:
go run main.go
你会看到一个漂亮的交互式列表!尝试用 j
/k
或方向键移动,用空格或回车键勾选,最后用 q
退出。是不是很简单?
进阶 Demo:加载动画和进度条
很多 CLI 工具需要执行一些耗时操作,比如下载文件、处理数据等。在这种情况下,给用户一个清晰的反馈至关重要。
下面的 Demo 将展示一个加载动画(spinner),模拟一个耗时任务,然后用一个进度条展示任务进度,最后显示完成信息。
这个例子会引入 tea.Tick
来创建定时事件,并使用 github.com/charmbracelet/bubbles
这个官方组件库,它提供了很多现成的 TUI 组件。
安装依赖:
go get github.com/charmbracelet/bubbles
完整代码 main.go
:
package main
import (
"fmt"
"os"
"time"
"github.com/charmbracelet/bubbles/progress"
"github.com/charmbracelet/bubbles/spinner"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
// 定义一些样式
var (
currentPkgNameStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("211"))
doneStyle = lipgloss.NewStyle().Margin(1, 2)
checkMark = lipgloss.NewStyle().Foreground(lipgloss.Color("42")).SetString("✓")
)
// Model 定义了应用的状态
type model struct {
packages []string // 模拟要处理的包名
index int // 当前处理到第几个包
spinner spinner.Model // 加载动画组件
progress progress.Model // 进度条组件
done bool // 是否全部完成
}
// 这是一个消息,表示包已经处理完成
type packageInstalledMsg struct{}
// 模拟安装一个包(耗时操作)
func installPackage(pkgName string) tea.Cmd {
// 模拟一个耗时的网络或磁盘I/O操作
d := time.Millisecond * 750
return tea.Tick(d, func(t time.Time) tea.Msg {
return packageInstalledMsg{}
})
}
func initialModel() model {
s := spinner.New()
s.Spinner = spinner.Dot
return model{
packages: []string{"lipgloss", "bubbletea", "bubbles", "glamour", "charm"},
spinner: s,
progress: progress.New(progress.WithDefaultGradient()),
}
}
func (m model) Init() tea.Cmd {
// 启动时,同时启动 spinner 的转动和第一个包的“安装”
return tea.Batch(
m.spinner.Tick,
installPackage(m.packages[m.index]),
)
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
// 按任意键退出
return m, tea.Quit
case tea.WindowSizeMsg:
// 窗口大小变化时,更新进度条宽度
m.progress.Width = msg.Width - 4
if m.progress.Width > 100 {
m.progress.Width = 100
}
return m, nil
case packageInstalledMsg: // 自定义的包安装完成消息
// 如果所有包都处理完了
if m.index >= len(m.packages)-1 {
m.done = true
return m, tea.Quit // 完成后退出
}
// 更新进度条
progressCmd := m.progress.SetPercent(float64(m.index+1) / float64(len(m.packages)))
// 推进到下一个包
m.index++
// 同时执行:更新进度条动画、启动下一个包的安装
return m, tea.Batch(
progressCmd,
installPackage(m.packages[m.index]),
)
case spinner.TickMsg: // spinner 的 tick 消息
var cmd tea.Cmd
m.spinner, cmd = m.spinner.Update(msg)
return m, cmd
case progress.FrameMsg: // progress bar 的 tick 消息
progressModel, cmd := m.progress.Update(msg)
m.progress = progressModel.(progress.Model)
return m, cmd
default:
return m, nil
}
}
func (m model) View() string {
if m.done {
return doneStyle.Render(fmt.Sprintf("搞定! 安装了 %d 个包. %s\n", len(m.packages), checkMark.String()))
}
pkgName := currentPkgNameStyle.Render(m.packages[m.index])
return fmt.Sprintf(
"正在安装... %s %s\n\n%s",
m.spinner.View(),
pkgName,
m.progress.View(),
)
}
func main() {
p := tea.NewProgram(initialModel())
if _, err := p.Run(); err != nil {
fmt.Println("运行出错:", err)
os.Exit(1)
}
}
运行它:
go mod tidy # 确保依赖下载
go run main.go
你会看到一个非常专业的安装过程:一个旋转的加载动画告诉你当前正在安装什么,下方的进度条则实时反馈总体进度。这用户体验,瞬间就上来了!
总结
Bubble Tea 的魅力在于其简洁的函数式思想。通过 Model
, Update
, View
的核心循环,你可以将复杂的 UI 状态管理变得井井有条。
再加上 bubbles
组件库和 lipgloss
样式库的加持,你几乎可以随心所欲地定制你的 TUI,无论是颜色、布局还是交互,都能轻松实现。
下次当你需要开发一个 CLI 工具时,别再只满足于打印日志了。试试 Bubble Tea,为你的用户(也为你自己)带来一次愉悦的终端交互体验吧!