Недавно я наткнулся на Bubble Tea — терминальный UI-фреймворк для Go, и буквально влюбился в то, как он отрисовывает интерфейсы в консоли. В репозитории есть множество примеров — и выглядят они действительно красиво.

Пример возможностей Bubble Tea
Пример возможностей Bubble Tea

Я никак не связан с разработчиками — просто захотел поделиться личным опытом.

Первое открытие: низкоуровневое управление и архитектура ELM

С первого взгляда Bubble Tea выглядит как высокоуровневый фреймворк с красивой абстракцией. Но под капотом он довольно низкоуровневый в плане контроля, и построен на архитектуре, вдохновлённой ELM.

Она основана на трёх основных концепциях:

  • Model — структура, содержащая всё состояние приложения (например, позицию курсора, список элементов, ввод пользователя и т. д.).

  • Update(msg) — функция, в которой можно изменять модель. Принимает сообщение (пользовательское действие или внутреннее событие), возвращает обновлённую модель и (опционально) команду.

  • View(model) — рендеринг интерфейса на основе текущей модели.

Что такое команды?

Команды — это события, которые мы можем генерировать и отправлять сами. Это может быть как пользовательское действие (например, нажатие клавиши), так и произвольная внутренняя структура. В Update они обрабатываются через type switch.

Пример: бесконечный спиннер загрузки

Допустим, нам нужен спиннер с тремя кадрами, которые циклично меняются каждую секунду. Вот как это реализуется в Bubble Tea:

  1. В модели храним текущий индекс кадра и список кадров.

  2. Объявляем кастомную команду type nextFrame struct {}.

  3. При инициализации модели вызываем первую команду nextFrame.

  4. В Update, при получении команды nextFrame, увеличиваем индекс и планируем следующую команду с задержкой в 1 секунду (с помощью утилит из фреймворка).

  5. Фреймворк сам вызывает View при изменениях, а мы в View просто показываем нужный кадр по индексу.

В результате — плавно анимированный спиннер, без явных таймеров и горутин.

Второе открытие: простая параллельность через команды

Команды оказались удивительно мощным инструментом. Например, предположим, нам нужно загрузить 10 изображений с помощью 3 воркеров. В обычном Go мы бы использовали каналы и горутины, но в Bubble Tea можно обойтись всего одной командой:

  • Команда проверяет: есть ли ещё изображения в очереди. Если да — загружает, сохраняет результат, вызывает себя снова.

  • При старте запускаем 3 таких команды — и получаем 3 параллельно работающих воркера.

  • Когда очередь заканчивается, команды просто перестают запускаться.

Это не "классическая" конкурентность Go, но она естественно вписывается в архитектуру UI-приложения, и позволяет сохранять отзывчивость интерфейса.

Третье открытие: вся архитектура — это ELM, и с этим надо смириться

Как только вы начнёте писать что-то чуть сложнее Hello World, всё ваше приложение станет ELM-подобным. Другого пути нет: либо вы принимаете архитектуру, либо постоянно боретесь с ней. Вот что помогло мне на этом пути:

  • Не полагайтесь на нейросети для генерации кода. Большинство LLM плохо справляются с архитектурой Bubble Tea. Код получается нечитаемым. Лучше продумать архитектуру, модель и структуру проекта самостоятельно, а LLM подключать точечно — например, для генерации вспомогательных функций или отладки.

  • Разделяйте ответственность. Выносите ViewUpdate и Model в отдельные файлы. Даже официальные примеры временами тяжело читать без этого.

  • Следите за читаемостью Update. Он быстро может превратиться в 300 строк кода. Используйте вспомогательные методы и выносите обработку отдельных команд.

  • Выносите стили в отдельный файл. Цвета, отступы, рамки — всё это лучше оформить централизованно.

  • Обязательно проводите рефакторинг. Написали рабочую реализацию — сделайте её читаемой. Иначе завтра вы сами себя не поймёте.

Эти советы в целом универсальны, но именно в Bubble Tea их игнорирование быстро делает поддержку невозможной.

Что я сделал на Bubble Tea

Я реализовал визуальный менеджер зависимостей для go.mod, который сканирует зависимости и показывает, какие можно и нужно обновить. Это может быть полезно, потому что стандартный go get -u просто обновляет всё подряд, и часто приводит к тому, что проект перестаёт собираться.

Проект здесь: chaindead/modup

Отдельная статья по проекту

Если у вас был опыт с Bubble Tea — расскажите, как оно? Какие архитектурные приёмы вам помогли?

Комментарии (4)


  1. allishappy
    21.08.2025 20:37

    Я пытался на нём делать таймер помодоро с большим количеством вложенных меню и окон (около 10). И столкнулся с проблемой, что сложную логику писать там проблематично. У меня не получилось как-то всё красиво организовать. Ну я и забил.

    Не зря у них все демки состоят из двух-трёх экранов. И не просто так тот же lazygit использует другую либу для отрисовки интерфейса в терминале.


    1. R3Z4boris Автор
      21.08.2025 20:37

      Мне кажется что у либы требует нетипичного подхода, который может развиться только после написания на ней проектов 2-3 на ней + ее больно дебажить

      По ней бы пригодился хороший гайд по струтуре сложных приложений, с кучей вьюх и лейаутов (типа project layout), но из вариантов только смотреть их examples и чужие репы с большими приложениями.

      Но мне очень понравилась ее гибкость и то как она заставляет голову думать об организации кода, эдакий мини челенж))


    1. BURAKKu
      21.08.2025 20:37

      Textual на py будет удобнее в работе. Но у него не самая хорошая оптимизация


      1. R3Z4boris Автор
        21.08.2025 20:37

        не очень люблю cli на пайтоне, накатываешь кли приложение, а с ним зависимостями python 3.10 какойнибудь выкачивается)