Go-bubbletea-使用

Bubble tea Github 地址
结合 Cobra, Viper 和 Bubbletea

Elm 简介

Elm 是一种函数式编程语言的名称, 其重要工作流程包括:

  1. Model, application 的状态
  2. View, 将状态转换为 text
  3. Update, 更新状态 based on messages

基本使用

结构示意:

引入

1
2
3
4
5
6
7
8
package main

import (
"fmt"
"os"

tea "github.com/charmbracelet/bubbletea"
)

主要方法

  • Init() Cmd, 最开始执行, 其返回的 Cmd 函数会返回 Msg 给 Update 使用(可能是异步执行)
  • Update(msg) (Model,Cmd), 处理事件以及更新 model, 其返回的 Cmd 也会立即执行
  • View() string, 根据 model 渲染 UI
    (这三个似乎会被自动调用)

利用这三个方法分别处理不同的功能.

运行逻辑: NewProgram 先初始化一个 model, 用 Run 运行后, 这个 model 会自动调用 Init() 函数一次, 之后由 Update() 函数接管, 当 I/O 发生时, Update() 函数会根据 msg tea.Msg 类型的值更新调用者的参数, 之后 View() 根据最新参数绘制出 UI.

主函数调用一般为:

1
2
3
4
5
6
7
func main() {
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() 来运行.

主要类型

  • bubbletea.Msg, I/O 会被自动捕获并存储在这个类型中 (interface {})
  • bubbletea.Model, 指实现了 Init, Update, View 函数的类型 (一个 interface)
  • bubbletea.Cmd, 返回值为 Msg 类型的 function, 也就是说, 这个函数运行结束后会返回一个有用信息, 如 tea.Quit 会返回 Msg 且不接受参数

创建一个模型

1
2
3
4
5
type model struct {
choices []string
cursor int
selected map[int]struct{}
}

初始化模型

1
2
3
4
5
6
func initialModel() model {
return model{
choices: []string{"Buy carrots", "Buy celery", "Buy kohlrabi"},
selected: make(map[int]struct{}),
}
}

定义 Init 方法

1
2
3
4
func (m model) Init() tea.Cmd {
// Just return `nil`, which means "no I/O right now, please."
return nil
}

定义 Update 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {

// Is it a key press?
case tea.KeyMsg:

// Cool, what was the actual key pressed?
switch msg.String() {

// These keys should exit the program.
case "ctrl+c", "q":
return m, tea.Quit

// The "up" and "k" keys move the cursor up
case "up", "k":
if m.cursor > 0 {
m.cursor--
}

// The "down" and "j" keys move the cursor down
case "down", "j":
if m.cursor < len(m.choices)-1 {
m.cursor++
}

// The "enter" key and the spacebar (a literal space) toggle
// the selected state for the item that the cursor is pointing at.
case "enter", " ":
_, ok := m.selected[m.cursor]
if ok {
delete(m.selected, m.cursor)
} else {
m.selected[m.cursor] = struct{}{}
}
}
}

// Return the updated model to the Bubble Tea runtime for processing.
// Note that we're not returning a command.
return m, nil
}

定义 View 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func (m model) View() string {
// The header
s := "What should we buy at the market?\n\n"

// Iterate over our choices
for i, choice := range m.choices {

// Is the cursor pointing at this choice?
cursor := " " // no cursor
if m.cursor == i {
cursor = ">" // cursor!
}

// Is this choice selected?
checked := " " // not selected
if _, ok := m.selected[i]; ok {
checked = "x" // selected!
}

// Render the row
s += fmt.Sprintf("%s [%s] %s\n", cursor, checked, choice)
}

// The footer
s += "\nPress q to quit.\n"

// Send the UI for rendering
return s
}

需要注意这里的返回值就是最终呈现的 UI.

定义 main 函数

1
2
3
4
5
6
7
func main() {
p := tea.NewProgram(initialModel())
if _, err := p.Run(); err != nil {
fmt.Printf("Alas, there's been an error: %v", err)
os.Exit(1)
}
}

NewProgram 接受一个 struct 类型为参数.

常见类型

  • tea.KeyMsg, 按键类型

Commands in Bubble Tea

若要创建一个可以接受参数的 Cmd, 如:

1
2
3
4
5
6
7
8
9
10
func checkSomeUrl(url string) tea.Cmd {
return func() tea.Msg {
c := &http.Client{Timeout: 10 * time.Second}
res, err := c.Get(url)
if err != nil {
return errMsg{err}
}
return statusMsg(res.StatusCode)
}
}

重要函数, 类型的原型

type Model

也就是实现了这三个函数的类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
type Model interface {
// Init is the first function that will be called. It returns an optional
// initial command. To not perform an initial command return nil.
Init() Cmd

// Update is called when a message is received. Use it to inspect messages
// and, in response, update the model and/or send a command.
Update(Msg) (Model, Cmd)

// View renders the program's UI, which is just a string. The view is
// rendered after every Update.
View() string
}

type Msg

也就是可以接受任意类型:

1
type Msg interface{}

type KeyMsg 和 type Key

KeyMsg 就是 Key 的另一种形式:

1
type KeyMsg Key

Key 类型是一个结构体:

1
2
3
4
5
type Key struct {
Type KeyType
Runes []rune
Alt bool
}

其中:

1
type KeyType int

KeyMsg, Key, KeyType 都含 String() 方法, 获取对应的键名, 如: enter, a 等.

type Program

同样是一个空结构体:

1
2
3
type Program struct {
// contains filtered or unexported fields
}

只用于标记类型.

type ProgramOption

即参数为 *Program 的函数:

1
type ProgramOption func(*Program)

创建一个 Program 类型值

用:

1
func NewProgram(model Model, opts ...ProgramOption) *Program

Kill 一个 bubbletea Program

1
func (p *Program) Kill()

退出一个 bubbletea Program

1
func (p *Program) Quit()

开始 event loop

1
func (p *Program) Run() (Model, error)

在 Program 运行后发送 Msg

1
func (p *Program) Send(msg Msg)

设置 Window Title

1
func (p *Program) SetWindowTitle(title string)

Delay Program

1
func (p *Program) Wait()

可用的 ProgramOption 函数

让 Program starts in full window mode

1
func WithAltScreen() ProgramOption

设置 FPS

1
func WithFPS(fps int) ProgramOption

If less than 1, the default value of 60 will be used. If over 120, the FPS will be capped at 120.

设置 filter

1
func WithFilter(filter func(Model, Msg) Msg) ProgramOption

其返回的 tea.Msg 替代原来的 Msg 给 bubble tea process 使用 (对原来的 Msg 进行处理), 但如果其返回的是 nil, 则会被忽略.

技巧积累

处理鼠标点击

有五个相关的类型, 以及一些处理函数.

若要使用 mouse, 需要在创建 program 是提供 ProgramOption, 如:

1
2
3
4
5
6
func main() {
p := tea.NewProgram(model{}, tea.WithMouseAllMotion())
if _, err := p.Run(); err != nil {
log.Fatal(err)
}
}

type MouseAction

本质是 int 类型:

1
type MouseAction int

预定义有:

1
2
3
4
5
const (
MouseActionPress MouseAction = iota
MouseActionRelease
MouseActionMotion
)

type MouseButton

本质也是 int 类型:

预定义有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const (
MouseButtonNone MouseButton = iota
MouseButtonLeft
MouseButtonMiddle
MouseButtonRight
MouseButtonWheelUp
MouseButtonWheelDown
MouseButtonWheelLeft
MouseButtonWheelRight
MouseButtonBackward
MouseButtonForward
MouseButton10
MouseButton11
)

type MouseEvent

一个包含前两个类型的结构体:

1
2
3
4
5
6
7
8
9
10
11
12
type MouseEvent struct {
X int
Y int
Shift bool
Alt bool
Ctrl bool
Action MouseAction
Button MouseButton

// Deprecated: Use MouseAction & MouseButton instead.
Type MouseEventType
}

IsWheel 判断是否有鼠标滚动

1
func (m MouseEvent) IsWheel() bool

String 返回表示鼠标事件的字符串

1
func (m MouseEvent) String() (s string)

type MouseMsg

1
type MouseMsg MouseEvent

其包含一个 mouse event, 且会送给 Update(tea.Msg) (tea.Model, tea.Cmd) 处理.

String 同样是返回表示鼠标事件的字符串

1
func (m MouseMsg) String() string

函数声明

Init, UpdateView 函数的声明是固定的.

1
2
3
4
5
6
7
8
func (m model) Init() tea.Cmd { 
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}

func (m model) View() string {
}

似乎只有 Update 中才会使用 msg tea.Msg 变量的信息.

Update 所返回的 Cmd 是会运行的.

颜色设置

可参考 example/altscreen-toggle.

1
2
3
4
5
6
var (
color = termenv.EnvColorProfile().Color
keyword = termenv.Style{}.Foreground(color("204")).Background(color("235")).Styled
help = termenv.Style{}.Foreground(color("241")).Styled
)

处理按键的步骤

获取 msg 类型: msg := msg.(type), 按键的类型为 tea.KeyMsg

获取具体的键值: msg.String()

常用 Cmd

退出程序: tea.Quit

全屏: tea.EnterAltScreen

开启事件循环

tea.NewProgram(model{}).Run()

需要先用一个结构类型来创建一个 Program, 再运行.

model 结构体的作用

用于在 Update() 函数中设置值, 并传递到 View 函数中用于判断 UI 的变化.

处理 input

可用库 github.com/charmbracelet/bubbles/textinput. Github 仓库位置.

charmbracelet/bubbles 有很多可用的 components 可供选择.

处理 layout

可用库 github.com/charmbracelet/lipgloss, Github 仓库位置

处理 http

用标准库 net/http

处理动画

可用库 github.com/charmbracelet/harmonica, Github 仓库位置

Cmd 函数

Tick 等间隔发送 Msg

1
func Tick(d time.Duration, fn func(time.Time) Msg) Cmd

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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
}

Sequence 一次运行一个 Cmd

1
func Sequence(cmds ...Cmd) Cmd

一个时间只运行一个 Cmd, 和 Batch 不同.


Go-bubbletea-使用
http://example.com/2023/10/02/Go-bubbletea使用/
作者
Jie
发布于
2023年10月2日
许可协议