你是否用过 lazygit/yazi/opencode 等类似软件
了解
- https://github.com/charmbracelet/bubbletea
- https://github.com/rivo/tview
- https://github.com/jesseduffield/gocui
bubbletea 入门
- 定义程序需要的数据模型 model
- 数据模型必须实现 model 接口3个方法(Init,Update,View)
- Init: 命令初始化
- Update: 处理所有的事件输入, 并返回更新后的 Model 和需要执行的 Command
- View: 将模型渲染成用户能看到的字符串
实现一个计数器
go
package main
import (
"fmt"
"os"
tea "github.com/charmbracelet/bubbletea"
)
// 所有的 bebbletea 程序都必须实现 model 这个接口
// type model interface {
// Init() Cmd
// Update(Msg) (Model, Cmd)
// View() string
// }
// 1.定义 type xxxModel struct (程序运行需要的状态)
// 2.实现 Model interface 3 个方法(Init, Update, View)
type model int
// 初始化模型,设置计数器的初始值
func initialModel() model {
return 0
}
// 3.Init: 命令初始化
// 初始化函数: 必须存在(接口中规定的)
func (m model) Init() tea.Cmd {
return nil
}
// 4.Update: 处理所有的事件输入, 并返回更新后的 Model 和需要执行的 Command
// 更新函数: 根据消息更新模型
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":
return m, tea.Quit
case "+":
return m + 1, nil
case "-":
return m - 1, nil
}
}
return m, nil
}
// 5.View: 将模型渲染成用户能看到的字符串
// 视图渲染函数
func (m model) View() string {
return fmt.Sprintf("Count: %d\nPress [+] to increment, [-] to decrement, [q] to quit.\n", m)
}
func main() {
p := tea.NewProgram(initialModel())
if _, err := p.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error running program: %v", err)
os.Exit(1)
}
}实现一个 ToDoList
- 增加 todo
- 删除 todo
- 切换 todo 状态
go
package main
import (
"fmt"
"os"
"strings"
tea "github.com/charmbracelet/bubbletea"
)
type todoItem struct {
isCompleted bool
title string
}
// 1.定义程序状态模型
type model struct {
todos []todoItem
current int
inputMode bool
inputBuff string
}
func initialModel() model {
// 初始化程序状态模型
todos := []todoItem{
{
title: "洗衣服",
isCompleted: false,
},
{
title: "做饭",
isCompleted: false,
},
{
title: "拖地",
isCompleted: false,
},
{
title: "写作业",
isCompleted: false,
},
{
title: "写代码",
isCompleted: false,
},
}
return model{
todos: todos,
current: 0, // 默认选中第一个
inputMode: false,
inputBuff: "",
}
}
// 初始化方法
func (m model) Init() tea.Cmd {
return nil
}
func (m *model) cursorMoveUp() {
if m.current > 0 {
m.current--
}
}
func (m *model) cursorMoveDown() {
max := len(m.todos) - 1
if m.current < max {
m.current++
}
}
func (m model) getTodoCompleted(i int) todoItem {
return m.todos[i]
}
func (m *model) toggleTodoCompleted(i int) {
todo := m.getTodoCompleted(i)
todo.isCompleted = !todo.isCompleted
m.todos[i] = todo
}
func (m *model) deleteTodo(i int) {
todoCount := len(m.todos)
if todoCount == 0 {
return
}
m.todos = append(m.todos[:i], m.todos[i+1:]...)
if m.current >= todoCount && todoCount > 0 {
m.current = todoCount - 1
return
}
if todoCount == 0 {
m.current = 0
return
}
}
func (m *model) addTodo(title string) {
if title == "" {
return
}
newItem := todoItem{
title: title,
isCompleted: false,
}
m.todos = append(m.todos, newItem)
m.current = len(m.todos) - 1
}
func (m *model) enterInputMode() {
m.inputMode = true
m.inputBuff = ""
}
func (m *model) resetInputMode() {
m.inputMode = false
m.inputBuff = ""
}
// 更新方法
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
// 如果处于输入模式, 专门处理输入按键
if m.inputMode {
switch msg.String() {
case "enter":
// 确认添加
m.addTodo(m.inputBuff)
m.resetInputMode()
return m, nil
case "esc":
// 取消添加
m.resetInputMode()
return m, nil
case "backspace":
// 删除最后一个字符
if len(m.inputBuff) > 0 {
m.inputBuff = m.inputBuff[:len(m.inputBuff)-1]
}
return m, nil
default:
// 只添加可打印字符(过滤控制键)
if len(msg.String()) == 1 && msg.String() >= " " && msg.String() <= "~" {
m.inputBuff += msg.String()
}
return m, nil
}
}
switch msg.String() {
case "ctrl+c", "ctrl+q", "q":
// 退出程序
return m, tea.Quit
case " ":
// 空格: 切换当前 todo 的选中状态
m.toggleTodoCompleted(m.current)
return m, nil
case "d":
// 删除当前 todo
m.deleteTodo(m.current)
return m, nil
case "a":
// 增加新的 todo: 进入输入模式
m.enterInputMode()
return m, nil
case "ctrl+j", "down":
// 光标向下
m.cursorMoveDown()
return m, nil
case "ctrl+k", "up":
// 光标向上
m.cursorMoveUp()
return m, nil
}
}
return m, nil
}
// View 方法
func (m model) View() string {
var sb strings.Builder
if m.inputMode {
// 输入模式界面
sb.WriteString("Enter new todo title:\n")
sb.WriteString("> " + m.inputBuff)
sb.WriteString("\n\n[Enter] confirm [Esc] cancel")
return sb.String()
}
// 正常列表界面
sb.WriteString("Todo List\n")
for i, todo := range m.todos {
cursor := " "
if i == m.current {
cursor = "▶"
}
isChecked := " "
if todo.isCompleted {
isChecked = "✔"
}
line := fmt.Sprintf("%s [%s] %s\n", cursor, isChecked, todo.title)
sb.WriteString(line)
}
// 没有 todo 时提示
if len(m.todos) == 0 {
sb.WriteString("(empty)\n")
}
sb.WriteString("\nPress [a] to add todo.\n")
sb.WriteString("Press [d] to delete.\n")
sb.WriteString("Press [q] to quit.\n")
return sb.String()
}
func main() {
p := tea.NewProgram(initialModel())
if _, err := p.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error running program: %v", err)
os.Exit(1)
}
}