简介
bubbletea
是一个简略、玲珑、能够十分不便地用来编写 TUI(terminal User Interface,控制台界面程序)程序的框架。内置简略的事件处理机制,能够对外部事件做出响应,如键盘按键。一起来看下吧。先看看 bubbletea
能做出什么成果:
感激 kiyonlin 举荐。
疾速应用
本文代码应用 Go Modules。
创立目录并初始化:
$ mkdir bubbletea && cd bubbletea
$ go mod init github.com/darjun/go-daily-lib/bubbletea
装置 bubbletea
库:
$ go get -u github.com/charmbracelet/bubbletea
bubbletea
程序都须要有一个实现 bubbletea.Model
接口的类型:
type Model interface {Init() Cmd
Update(Msg) (Model, Cmd)
View() string}
Init()
办法在程序启动时会立即调用,它会做一些初始化工作,并返回一个Cmd
通知bubbletea
要执行什么命令;Update()
办法用来响应内部事件,返回一个批改后的模型,和想要bubbletea
执行的命令;View()
办法用于返回在管制台上显示的文本字符串。
上面咱们来实现一个 Todo List。首先定义模型:
type model struct {todos []string
cursor int
selected map[int]struct{}}
todos
:所有待实现事项;cursor
:界面上光标地位;selected
:已实现标识。
不须要任何初始化工作,实现一个空的 Init()
办法,并返回nil
:
import (tea "github.com/charmbracelet/bubbletea")
func (m model) Init() tea.Cmd {return nil}
咱们须要响应按键事件,实现 Update()
办法。按键事件产生时会以相应的 tea.Msg
为参数调用 Update()
办法。通过对参数 tea.Msg
进行类型断言,咱们能够对不同的事件进行对应的解决:
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.todos)-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
}
约定:
ctrl+c
或q
:退出程序;up
或k
:向上挪动光标;down
或j
:向下挪动光标;enter
或
解决 ctrl+c
或q
按键时,返回一个非凡的 tea.Quit
,告诉bubbletea
须要退出程序。
最初实现 View()
办法,这个办法返回的字符串就是最终显示在管制台上的文本。咱们能够依照本人想要的模式,依据模型数据拼装:
func (m model) View() string {
s := "todo list:\n\n"
for i, choice := range m.todos {
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
}
光标所在位置用 >
标识,已实现的事项减少 x
标识。
模型类型定义好了之后,须要创立一个该模型的对象;
var initModel = model{todos: []string{"cleanning", "wash clothes", "write a blog"},
selected: make(map[int]struct{}),
}
为了让程序工作,咱们还要创立一个 bubbletea
的利用对象,通过 bubbletea.NewProgram()
实现,而后调用这个对象的 Start()
办法开始执行:
func main() {cmd := tea.NewProgram(initModel)
if err := cmd.Start(); err != nil {fmt.Println("start failed:", err)
os.Exit(1)
}
}
运行:
GitHub Trending
一个简略的 Todo 利用看起来如同没什么意思。接下来,咱们一起编写一个拉取 GitHub Trending 仓库并显示在控制台的程序。
Github Trending 的界面如下:
能够抉择语言(Spoken Language,本地语言)、语言(Language,编程语言)和工夫范畴(Today,This week,This month)。因为 GitHub 没有提供 trending 的官网 API,咱们只能爬取网页本人来剖析。好在 Go 有一个弱小的剖析工具 goquery,提供了堪比 jQuery 的弱小性能。我之前也写过一篇文章介绍它——Go 每日一库之 goquery。
关上 Chrome 控制台,点击 Elements 页签,查看每个条目标构造:
根底版本
定义模型:
type model struct {repos []*Repo
err error
}
其中 repos
字段示意拉取到的 Trending 仓库列表,构造体 Repo
如下,字段含意都有正文,很清晰了:
type Repo struct {
Name string // 仓库名
Author string // 作者名
Link string // 链接
Desc string // 形容
Lang string // 语言
Stars int // 星数
Forks int // fork 数
Add int // 周期内新增
BuiltBy []string // 奉献值 avatar img 链接}
err
字段示意拉取失败设置的谬误值。为了让程序启动时,就去执行网络申请拉取 Trending 的列表,咱们让模型的 Init()
办法返回一个 tea.Cmd
类型的值:
func (m model) Init() tea.Cmd {return fetchTrending}
func fetchTrending() tea.Msg {repos, err := getTrending("","daily")
if err != nil {return errMsg{err}
}
return repos
}
tea.Cmd
类型为:
// src/github.com/charmbracelet/bubbletea/tea.go
type Cmd func() Msg
tea.Cmd
底层是一个函数类型,函数无参数,并且返回一个 tea.Msg
对象。
fetchTrending()
函数拉取 GitHub 的今日 Trending 列表,如果遇到谬误,则返回 error
值。这里咱们临时疏忽 getTrending()
函数的实现,这个与咱们要说的重点关系不大,感兴趣的童鞋能够去我的 GitHub 仓库查看具体代码。
程序启动时如果须要做一些操作,通常就会在 Init()
办法中返回一个 tea.Cmd
。tea
后盾会执行这个函数,最终将返回的 tea.Msg
传给模型的 Update()
办法。
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "q", "ctrl+c", "esc":
return m, tea.Quit
default:
return m, nil
}
case errMsg:
m.err = msg
return m, nil
case []*Repo:
m.repos = msg
return m, nil
default:
return m, nil
}
}
Update()
办法也比较简单,首先还是须要监听按键事件,咱们约定按下 q 或 ctrl+c 或 esc 退出程序。具体按键对应的字符串示意能够查看文档或源码 bubbletea/key.go
文件 。接管到errMsg
类型的音讯,示意网络申请失败了,记录谬误值。接管到 []*Repo
类型的音讯,示意正确返回的 Trending 仓库列表,记录下来。在 View()
函数中,咱们显示正在拉取,拉取失败和正确拉取等信息:
func (m model) View() string {
var s string
if m.err != nil {s = fmt.Sprintf("Fetch trending failed: %v", m.err)
} else if len(m.repos) > 0 {
for _, repo := range m.repos {s += repoText(repo)
}
s += "--------------------------------------"
} else {s = "Fetching GitHub trending ..."}
s += "\n\n"
s += "Press q or ctrl + c or esc to exit..."
return s + "\n"
}
逻辑很清晰,如果 err
字段不为 nil
示意失败,否则有仓库数据,显示仓库信息。否则正在拉取中。最初显示一条提示信息,通知客户怎么退出程序。
每个仓库项的显示逻辑如下,分为 3 列,根底信息、形容和链接:
func repoText(repo *Repo) string {
s := "--------------------------------------\n"
s += fmt.Sprintf(`Repo: %s | Language: %s | Stars: %d | Forks: %d | Stars today: %d
`, repo.Name, repo.Lang, repo.Stars, repo.Forks, repo.Add)
s += fmt.Sprintf("Desc: %s\n", repo.Desc)
s += fmt.Sprintf("Link: %s\n", repo.Link)
return s
}
运行(多文件运行不能用go run main.go
):
获取失败(国内 GitHub 不稳固,多试几次总会遇到😭):
获取胜利:
让界面更好看
黑红色咱们曾经看了太多太多了,能不能让字体出现不同的色彩呢?当然能够。bubbletea
能够利用 lipgloss
库给文本增加各种色彩,咱们定义了 4 种颜色,色彩的 RBG 值是我在 http://tool.chinaz.com/tools/pagecolor.aspx 挑的:
var (cyan = lipgloss.NewStyle().Foreground(lipgloss.Color("#00FFFF"))
green = lipgloss.NewStyle().Foreground(lipgloss.Color("#32CD32"))
gray = lipgloss.NewStyle().Foreground(lipgloss.Color("#696969"))
gold = lipgloss.NewStyle().Foreground(lipgloss.Color("#B8860B"))
)
想要将文本变为什么色彩,只须要调用对应色彩对象的 Render()
办法将文本传入即可。例如咱们想让提醒变为暗灰色,两头文字应用暗黄色,批改 View()
办法:
func (m model) View() string {
var s string
if m.err != nil {s = gold.Render(fmt.Sprintf("fetch trending failed: %v", m.err))
} else if len(m.repos) > 0 {
for _, repo := range m.repos {s += repoText(repo)
}
s += cyan.Render("--------------------------------------")
} else {s = gold.Render("Fetching GitHub trending ...")
}
s += "\n\n"
s += gray.Render("Press q or ctrl + c or esc to exit...")
return s + "\n"
}
而后仓库的根本信息咱们用青色(cyan),形容用绿色,链接用暗灰色:
func repoText(repo *Repo) string {s := cyan.Render("--------------------------------------") + "\n"
s += fmt.Sprintf(`Repo: %s | Language: %s | Stars: %s | Forks: %s | Stars today: %s
`, cyan.Render(repo.Name), cyan.Render(repo.Lang), cyan.Render(strconv.Itoa(repo.Stars)),
cyan.Render(strconv.Itoa(repo.Forks)), cyan.Render(strconv.Itoa(repo.Add)))
s += fmt.Sprintf("Desc: %s\n", green.Render(repo.Desc))
s += fmt.Sprintf("Link: %s\n", gray.Render(repo.Link))
return s
}
再次运行:
胜利:
嗯,当初难看多了。
我没有偷懒
有时候网络很慢,加上一个申请正在解决的提醒能让咱们更释怀(程序还在跑,没偷懒)。bubbletea
的兄弟仓库 bubbles
提供了一个叫做 spinner
的组件,它只是显示一些字符,始终在变动,给咱们造成一种工作正在解决中的感觉。spinner
在 github.com/charmbracelet/bubbles/spinner
包中,须要先引入。而后在模型中减少 spinner.Model
字段:
type model struct {repos []*Repo
err error
spinner spinner.Model
}
创立模型时,同时须要初始化 spinner.Model
对象,咱们指定 spinner
的文本色彩为紫色:
var purple = lipgloss.NewStyle().Foreground(lipgloss.Color("#800080"))
func newModel() model {sp := spinner.NewModel()
sp.Style = purple
return model{spinner: sp,}
}
spinner
通过 Tick
来触发其扭转状态,所以须要在 Init()
办法中返回触发 Tick
的Cmd
。然而又须要返回 fetchTrending
。bubbletea
提供了 Batch
能够将两个 Cmd
合并在一起返回:
func (m model) Init() tea.Cmd {
return tea.Batch(
spinner.Tick,
fetchTrending,
)
}
而后 Update()
办法中咱们须要更新 spinner
。Init()
办法返回的 spinner.Tick
会产生spinner.TickMsg
,咱们对其做解决:
case spinner.TickMsg:
var cmd tea.Cmd
m.spinner, cmd = m.spinner.Update(msg)
return m, cmd
spinner.Update(msg)
返回一个 tea.Cmd
对象驱动下一次Tick
。
最初在 View()
办法中,咱们将 spinner
显示进去。调用其 View()
办法返回以后状态的字符串,拼在咱们想要显示的地位:
func (m model) View() string {
var s string
if m.err != nil {s = gold.Render(fmt.Sprintf("fetch trending failed: %v", m.err))
} else if len(m.repos) > 0 {
for _, repo := range m.repos {s += repoText(repo)
}
s += cyan.Render("--------------------------------------")
} else {
// 这里
s = m.spinner.View() + gold.Render("Fetching GitHub trending ...")
}
s += "\n\n"
s += gray.Render("Press q or ctrl + c or esc to exit...")
return s + "\n"
}
运行:
分页
因为一次返回了很多 GitHub 仓库,咱们想对其进行分页显示,每页显示 5 条,能够按 pageup
和pagedown
翻页。首先在模型中减少两个字段,当前页和总页数:
const (CountPerPage = 5)
type model struct {
// ...
curPage int
totalPage int
}
拉取到仓库时,计算总页数:
case []*Repo:
m.repos = msg
m.totalPage = (len(msg) + CountPerPage - 1) / CountPerPage
return m, nil
另外须要监听翻页按键:
case "pgdown":
if m.curPage < m.totalPage-1 {m.curPage++}
return m, nil
case "pgup":
if m.curPage > 0 {m.curPage--}
return m, nil
在 View()
办法中,咱们依据当前页计算须要显示哪些仓库:
start, end := m.curPage*CountPerPage, (m.curPage+1)*CountPerPage
if end > len(m.repos) {end = len(m.repos)
}
for _, repo := range m.repos[start:end] {s += repoText(repo)
}
s += cyan.Render("--------------------------------------")
最初,如果总页数大于 1,给出翻页按键的提醒:
if m.totalPage > 1 {s += gray.Render("Pagedown to next page, pageup to prev page.")
s += "\n"
}
运行:
很棒,咱们只显示了 5 页。试试翻页吧:
总结
bubbletea
提供了一个 TUI 程序运行的根本框架。咱们要显示什么,显示的款式,要对哪些事件进行解决都由咱们本人指定。bubbletea
仓库的 examples
文件夹中有多个示例程序,对编写 TUI 程序感兴趣的童鞋千万不能错过。另外它的兄弟仓库 bubbles
中也提供了不少组件。
大家如果发现好玩、好用的 Go 语言库,欢送到 Go 每日一库 GitHub 上提交 issue😄
参考
- bubbletea GitHub:https://github.com/charmbracelet/bubbletea
- bubble GitHub:https://github.com/charmbracelet/bubbles
- Go 每日一库 GitHub:https://github.com/darjun/go-daily-lib
- issue:https://github.com/darjun/go-daily-lib/issues/22
我
我的博客:https://darjun.github.io
欢送关注我的微信公众号【GoUpUp】,独特学习,一起提高~