简介
The fun, functional and stateful way to build terminal apps. A Go framework based on The Elm Architecture. Bubble Tea is well-suited for simple and complex terminal applications, either inline, full-window, or a mix of both.
BubbleTea是一个轻量级的TUI(Terminal User Interface)框架,在其基础上,可以轻松的开发一些好看的命令行工具。
Bubble符合Elm框架(一种函数式的前端框架)的标准。
基础
首先熟悉一下官方文档中的例子。我们在这里创建了一个简单的选项表,模拟让用户选择不同食物。
程序入口
func main() {
// 此处的initialModel()返回一个Model
model := initialModel()
p := tea.NewProgram(initialModel())
if _, err := p.Run(); err != nil {
fmt.Printf("Alas, there's been an error: %v", err)
os.Exit(1)
}
}
用tea.NewProgram()
创建一个program,然后使用p.Run()
来启动程序。tea.NewProgram()
的参数通常是Model
类型。
Model
Model是一个接口,接口定义如下
// Model contains the program's state as well as its core functions.
type Model interface {
Init() Cmd
Update(Msg) (Model, Cmd)
View() string
}
也就是说,只要我们的类型实现了Init()
, Update(Msg)
, View()
方法,就可以把其作为Model参数传入tea.NewProgram()
中。
创建一个model结构体,并实现Model接口。
type model struct {
choices []string
cursor int
selected map[int]struct{}
}
// 首先被调用的函数,返回optional
// 如果不执行初始命令,则返回 nil。
func (m model) Init() tea.Cmd {
return nil
}
// 收到消息时调用Update()。用它来检查消息
// Update()将更新Model或者执行指令
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "q":
return m, tea.Quit
case "up", "k":
if m.cursor > 0 {
m.cursor--
}
case "down", "j":
if m.cursor < len(m.choices)-1 {
m.cursor++
}
case "enter", " ":
_, ok := m.selected[m.cursor]
if ok {
delete(m.selected, m.cursor)
} else {
m.selected[m.cursor] = struct{}{}
}
}
}
return m, nil
}
// View()用来渲染UI,每次调用Update()后都会调用View()
func (m model) View() string {
s := "What should we buy at the market?\n\n"
for i, choice := range m.choices {
cursor := " "
if m.cursor == i {
cursor = ">"
}
checked := " "
if _, ok := m.selected[i]; ok {
checked = "x"
}
s += fmt.Sprintf("%s [%s] %s\n", cursor, checked, choice)
}
s += "\nPress q to quit.\n"
return s
}
Update()
接受的msg可以是任何类型的,通常使用断言进行类型判断,从而对不同类型的msg做处理。在例子中,tea.KeyMsg
指的是是键盘输入,里面对应不同的类型做了不同逻辑判断。
View()
是渲染UI的函数,这里通过对m。curosr为的值进行判断,从而显示游标>
的位置。
Initialization函数
实例代码中还提供了一个Initialization函数,用来初始化model中的数据,但是这也不是必须的。我们可以通过很多别的方法创建、编辑model中的值
func initialModel() model {
return model{
// Our to-do list is a grocery list
choices: []string{"Buy carrots", "Buy celery", "Buy kohlrabi"},
// A map which indicates which choices are selected. We're using
// the map like a mathematical set. The keys refer to the indexes
// of the `choices` slice, above.
selected: make(map[int]struct{}),
}
}
当然,实例中的TUI过于简陋,实际上BubbleTea开发的TUI都很漂亮。BubbleTea的GitHub页面提供了非常多的实例,供开发者学习。
进度条(官方用例)
一个渐变色的进度条(animated),动画过渡平滑
另一种实现方式(静态的),每次update才渲染一次。
实现(animated)
全局变量
const (
padding = 2 // 填充长度
maxWidth = 80 // 进度条宽度
)
var helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#626262")).Render // 帮助字段样式
type tickMsg time.Time // msg类型
model定义
// 引入了"github.com/charmbracelet/bubbles/progress"中的progress这个model
type model struct {
progress progress.Model
}
github.com/charmbracelet/bubbles/ 本身也有不少已经实现的model,比如list, progress, spinner, paginator, spinner, table等,可以快速构建一个TUI应用。
Init函数实现
func (m model) Init() tea.Cmd {
return tickCmd()
}
这里返回了tickCmd()
函数,这个函数定义如下:
func tickCmd() tea.Cmd {
return tea.Tick(time.Second*1, func(t time.Time) tea.Msg {
return tickMsg(t)
})
}
后面的函数作为参数传入,目的只是返回一个time.time
类型,这里我其实不理解为什么开发者会把这个函数设计的这么复杂,在tea.Tick
的实现中,有大段的话解释,大致内容如下:
Tick
产生的定时器独立于系统时间Tick
函数需要传入一个时间间隔和一个函数作为参数。这个函数会返回一个消息,在这个消息中包含了定时器触发的时间Tick
函数只会发送单个消息,并不会自动按照间隔发送多个消息。为了实现定期触发消息,需要在接收到消息后返回另一个 Tick Cmd。
也就是说,这样的写法可以当成固定的组合用法
type TickMsg time.Time
func doTick() Cmd {
return Tick(time.Second, func(t time.Time) Msg {
return TickMsg(t)
})
}
func (m model) Init() Cmd {
// Start ticking.
return doTick()
}
func (m model) Update(msg Msg) (Model, Cmd) {
switch msg.(type) {
case TickMsg:
// Return your Tick command again to loop.
return m, doTick()
}
return m, nil
}
Update函数
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 - padding*2 - 4
if m.progress.Width > maxWidth {
m.progress.Width = maxWidth
}
return m, nil
case tickMsg:
if m.progress.Percent() == 1.0 {
return m, tea.Quit
}
// Note that you can also use progress.Model.SetPercent to set the
// percentage value explicitly, too.
cmd := m.progress.IncrPercent(0.25)
return m, tea.Batch(tickCmd(), cmd)
// FrameMsg is sent when the progress bar wants to animate itself
case progress.FrameMsg:
progressModel, cmd := m.progress.Update(msg)
m.progress = progressModel.(progress.Model)
return m, cmd
default:
return m, nil
}
}
在Update函数中,分别处理了窗口大小事件,点按键盘事件,时间跳动事件等。
View方法
func (m model) View() string {
pad := strings.Repeat(" ", padding)
return "\n" +
pad + m.progress.View() + "\n\n" +
pad + helpStyle("Press any key to quit")
}
在process
的方法的基础上,添加了占位符,添加了帮助信息。
命令行迷宫
package tui
import (
"strings"
"vimMaze/maze"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
const (
wall = iota
path
person
end
key
)
// 设置帮助字段样式,砖块样式
var helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#626262")).Render
var brickStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#B6482F")).Render
type model struct {
Maze maze.Maze
}
func (m model) Init() tea.Cmd {
return nil
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch key := msg.String(); key {
case "ctrl+c":
return m, tea.Quit
case "j":
m.Maze.GoDown()
return m, nil
case "k":
m.Maze.GoUp()
return m, nil
case "h":
m.Maze.Goleft()
return m, nil
case "l":
m.Maze.Goright()
return m, nil
}
case tea.WindowSizeMsg:
m.Maze = maze.GenerateMaze(min(msg.Height, msg.Width))
return m, nil
}
var cmd tea.Cmd
return m, cmd
}
func (m model) View() string {
if m.Maze.Win == true {
return "You win!🎉" + "\n\n " + helpStyle("Press ctrl+c to exit")
}
var build strings.Builder
for _, valueRow := range m.Maze.Map {
for _, valuePoint := range valueRow {
if valuePoint == wall {
build.WriteString(brickStyle("░░"))
} else if valuePoint == path {
build.WriteString(" ")
} else if valuePoint == person {
build.WriteString("🧑")
} else if valuePoint == end {
build.WriteString("💰")
}
}
build.WriteString("\n")
}
return build.String()
}
效果预览
TODO: 为nmap开发一个简单TUI,方便用户定义参数
可能遥遥无期了