charm/bubbletea для Go: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

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, чтение файлов) используйте горутины и

Рекомендаций