Skip to content

你是否用过 lazygit/yazi/opencode 等类似软件 ​

  • lazygit 高效 Git 操作 TUI 客户端(支持自定义快捷键)
  • yazi 高效文件管理 TUI软件(支持插件)
  • opencode 开源 vibe coding agent

了解 ​

bubbletea 入门 ​

  1. 定义程序需要的数据模型 model
  2. 数据模型必须实现 model 接口3个方法(Init,Update,View)
  3. Init: 命令初始化
  4. Update: 处理所有的事件输入, 并返回更新后的 Model 和需要执行的 Command
  5. 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)
	}
}

Released under the MIT License.