charm/bubbletea для Go: полное руководство с примерами
1. Что это и зачем нужна
Bubble Tea — это мощный, элегантный и продуктивный фреймворк для создания терминальных пользовательских интерфейсов (TUI) на языке Go. Вдохновленный архитектурой функционального программирования Elm, он предлагает простую, но невероятно гибкую модель, основанную на трех ключевых концепциях: Model (состояние), Update (функция обновления) и View (функция отображения).
Зачем использовать Bubble Tea? Если вам надоели скучные, однострочные CLI-утилиты, и вы хотите создавать интерактивные, красивые и отзывчивые приложения прямо в терминале — это ваш инструмент. Bubble Tea позволяет строить все, от простых форм ввода и списков выбора до сложных файловых менеджеров, музыкальных плееров, игр и панелей мониторинга. Он берет на себя всю низкоуровневую работу с терминалом (обработку сигналов, управление курсором, рендеринг), позволяя разработчику сосредоточиться на логике приложения.
Библиотека является частью экосистемы Charm, которая также включает в себя другие полезные инструменты, такие как Bubbles (набор готовых компонентов, таких как текстовые поля, спиннеры, прогресс-бары) и Lip Gloss (библиотека для стилизации вывода). Вместе они образуют полный набор для создания профессиональных TUI-приложений.
2. Установка
Установка библиотеки производится стандартным способом для Go:
go get github.com/charmbracelet/bubbletea@latest
Для использования готовых компонентов (виджетов) также рекомендуется установить пакет bubbles:
go get github.com/charmbracelet/bubbles@latest
И для стилизации:
go get github.com/charmbracelet/lipgloss@latest
3. Быстрый старт — минимальный рабочий пример
Создайте файл main.go и вставьте следующий код. Это приложение будет выводить количество нажатий клавиш. Нажмите Ctrl+C для выхода.
package main
import (
"fmt"
"os"
tea "github.com/charmbracelet/bubbletea"
)
// 1. Model (Модель) — хранит состояние приложения.
type model struct {
count int
}
// 2. Init (Инициализация) — возвращает начальную команду (обычно nil).
func (m model) Init() tea.Cmd {
return nil
}
// 3. Update (Обновление) — обрабатывает входящие сообщения и возвращает новую модель и команду.
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c":
return m, tea.Quit // Команда для выхода
default:
m.count++
return m, nil
}
}
return m, nil
}
// 4. View (Представление) — возвращает строку, которая будет отображена в терминале.
func (m model) View() string {
return fmt.Sprintf("Нажатий клавиш: %d\n\nНажмите любую клавишу. Для выхода нажмите Ctrl+C.", m.count)
}
func main() {
p := tea.NewProgram(model{})
if _, err := p.Run(); err != nil {
fmt.Printf("Ошибка: %v", err)
os.Exit(1)
}
}
Запустите приложение:
go run main.go
4. Основные методы и типы
В Bubble Tea ключевыми являются интерфейсы и функции, а не классы. Рассмотрим 8 основных элементов.
4.1. tea.NewProgram
Сигнатура: func NewProgram(model Model, opts ...ProgramOption) *Program
Что делает: Создает новый экземпляр программы Bubble Tea. Принимает начальную модель и опциональные настройки (например, для отключения альтернативного экрана).
// Создание программы с моделью и опцией для вывода в обычный терминал (без альтернативного экрана)
p := tea.NewProgram(model{}, tea.WithAltScreen())
4.2. Program.Run()
Сигнатура: func (p *Program) Run() (Model, error)
Что делает: Запускает основной цикл событий приложения. Блокирует выполнение до тех пор, пока не будет вызвана команда tea.Quit или не произойдет ошибка. Возвращает финальное состояние модели.
// Запуск программы
finalModel, err := p.Run()
if err != nil {
panic(err)
}
_ = finalModel // Финальное состояние модели
4.3. tea.Quit
Сигнатура: var Quit Cmd
Что делает: Это специальная команда (тип tea.Cmd), которая сигнализирует программе о необходимости завершить работу. Должна возвращаться из функции Update.
// Внутри функции Update:
case tea.KeyMsg:
if msg.String() == "q" {
return m, tea.Quit
}
4.4. tea.WindowSizeMsg
Сигнатура: type WindowSizeMsg struct { Width int; Height int }
Что делает: Сообщение, которое отправляется программе при изменении размера терминала. Очень важно для адаптивной верстки. Обрабатывается в функции Update.
// Внутри функции Update:
case tea.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height
return m, nil
4.5. tea.KeyMsg
Сигнатура: type KeyMsg struct { Type KeyType; rune rune; Alt bool; ... }
Что делает: Сообщение, отправляемое при нажатии клавиши. Содержит информацию о типе клавиши (буква, стрелка, Enter и т.д.) и модификаторах (Alt).
// Обработка нажатия Enter
case tea.KeyMsg:
switch msg.Type {
case tea.KeyEnter:
// Действие при нажатии Enter
case tea.KeyRunes:
// msg.Runes содержит набранные символы
m.input += string(msg.Runes)
}
4.6. tea.Tick
Сигнатура: func Tick(d time.Duration, fn func(time.Time) Msg) Cmd
Что делает: Создает команду, которая отправляет сообщение через заданный интервал времени. Используется для анимаций, таймеров, опросов внешних источников.
// Команда для отправки сообщения каждые 100 мс
func tickCmd() tea.Msg {
time.Sleep(100 * time.Millisecond)
return tickMsg{}
}
// В Init или Update:
return m, tea.Tick(time.Second/10, func(t time.Time) tea.Msg {
return tickMsg{}
})
4.7. tea.Batch
Сигнатура: func Batch(cmds ...Cmd) Cmd
Что делает: Объединяет несколько команд в одну. Позволяет выполнять несколько действий (например, запустить таймер и запросить данные) одновременно из одной функции Update.
// Возврат двух команд одновременно
return m, tea.Batch(tickCmd(), someOtherCmd())
4.8. tea.Println
Сигнатура: func Println(args ...interface{}) Cmd
Что делает: Создает команду для вывода текста за пределы TUI-области (например, в stdout). Полезно для логирования или вывода финальных результатов после завершения программы.
// Вывод сообщения в stdout перед выходом
case tea.KeyMsg:
if msg.String() == "q" {
return m, tea.Sequence(tea.Println("Выход по запросу пользователя"), tea.Quit)
}
5. Реальный практический пример: Интерактивный список задач (Todo List)
Этот пример демонстрирует создание полноценного TUI-приложения для управления списком задач. Вы можете добавлять, удалять и отмечать задачи как выполненные.
package main
import (
"fmt"
"os"
"strings"
tea "github.com/charmbracelet/bubbletea"
)
// Модель для задачи
type task struct {
text string
done bool
}
// Модель приложения
type model struct {
tasks []task
textInput strings.Builder
cursor int // Позиция курсора в списке
mode string // "normal" или "input"
err error
}
func initialModel() model {
return model{
tasks: []task{
{text: "Изучить Bubble Tea", done: false},
{text: "Написать статью", done: true},
},
mode: "normal",
}
}
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 m.mode {
case "normal":
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.tasks)-1 {
m.cursor++
}
case " ":
// Переключение статуса задачи
if m.cursor >= 0 && m.cursor < len(m.tasks) {
m.tasks[m.cursor].done = !m.tasks[m.cursor].done
}
case "d":
// Удаление задачи
if len(m.tasks) > 0 && m.cursor < len(m.tasks) {
m.tasks = append(m.tasks[:m.cursor], m.tasks[m.cursor+1:]...)
if m.cursor >= len(m.tasks) && len(m.tasks) > 0 {
m.cursor = len(m.tasks) - 1
}
}
case "a":
// Переход в режим добавления
m.mode = "input"
m.textInput.Reset()
}
case "input":
switch msg.String() {
case "enter":
// Добавление новой задачи
newTaskText := strings.TrimSpace(m.textInput.String())
if newTaskText != "" {
m.tasks = append(m.tasks, task{text: newTaskText, done: false})
}
m.mode = "normal"
case "esc":
m.mode = "normal"
case "backspace":
s := m.textInput.String()
if len(s) > 0 {
m.textInput.Reset()
m.textInput.WriteString(s[:len(s)-1])
}
default:
// Добавление символа
if msg.Type == tea.KeyRunes {
m.textInput.WriteString(string(msg.Runes))
}
}
}
}
return m, nil
}
func (m model) View() string {
var b strings.Builder
b.WriteString("📋 Список задач\n\n")
if m.mode == "input" {
b.WriteString("Введите новую задачу: ")
b.WriteString(m.textInput.String())
b.WriteString("\n\n")
} else {
b.WriteString("Используйте ↑/↓ для навигации, 'a' для добавления, 'd' для удаления, пробел для отметки, 'q' для выхода.\n\n")
for i, t := range m.tasks {
cursor := " "
if m.cursor == i {
cursor = "▶"
}
check := " "
if t.done {
check = "✓"
}
b.WriteString(fmt.Sprintf("%s [%s] %s\n", cursor, check, t.text))
}
}
if m.err != nil {
b.WriteString(fmt.Sprintf("\nОшибка: %v", m.err))
}
return b.String()
}
func main() {
p := tea.NewProgram(initialModel())
if _, err := p.Run(); err != nil {
fmt.Printf("Ошибка: %v", err)
os.Exit(1)
}
}
6. Советы по производительности и типичные ошибки
Советы по производительности:
- Избегайте аллокаций в View(): Функция
View()вызывается при каждом рендеринге. Используйтеstrings.Builderдля эффективного построения строк, избегайте конкатенации через+в циклах. - Не делайте блокирующих операций в Update(): Функция
Update()должна быть быстрой. Для длительных операций (запросы к API, чтение файлов) используйте горутины и