用BubbleTea写一个漂亮的TUI程序

用BubbleTea写一个漂亮的TUI程序

简介#

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,方便用户定义参数#

可能遥遥无期了

相关推荐

神武跑100环多少经验_神武跑100环多少经验值
office365E5无限续期

神武跑100环多少经验_神武跑100环多少经验值

⏳ 01-28 👁️ 6111
lol手游刀锋舞者怎么玩
best365投注

lol手游刀锋舞者怎么玩

⏳ 07-08 👁️ 9812