Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 13 из 25 ~18 мин +50 XP

bubbles/table: таблицы данных

Рано или поздно у каждой терминальной программы появляются данные, которые не влезают в список: имя, размер, дата, статус — четыре значения на одну сущность, и все просятся в свои колонки. Для этого в наборе bubbles есть пакет table: готовая таблица данных для Bubble Tea с курсором, прокруткой и выделением строки. Соберём таблицу задач, настроим размеры и стили и научимся обновлять строки по мере прихода данных.

Содержание урока · 10 разделов

После списка из девятого урока у панели задач осталась одна неувязка: элемент — это одна строка с заголовком и описанием, а любая табличная сущность живёт колонками. Очередь сборок: имя, ветка, статус, длительность. Пачка бэкапов: файл, размер, время, результат. Писать выравнивание через Sprintf можно ровно до первого длинного значения, ломающего всю сетку. Пакет bubbles/table решает задачу целиком: он рисует заголовок, держит курсор на строке, прокручивает данные внутри фиксированной высоты, подсвечивает выбранную строку и умеет обновлять содержимое на лету. По устройству это обёртка над знакомым по одиннадцатому уроку viewport: сверху строка заголовков, внутри — прокручиваемый блок строк. Если вы собирали подмодели по восьмому уроку, ничего нового в схеме встраивания не будет — новыми здесь будут только сами колонки и правила их ширины.

table.Model: колонки и строки#

Таблица описывается двумя срезами. Колонка — тип table.Column с полями Title (заголовок) и Width (ширина колонки в символах). Строка — тип table.Row, под которым прячется просто []string: значения по порядку колонок. Ключевое правило: строки рисуются по числу колонок, и значение, не влезающее в свою Width, честно обрезается с многоточием — сетка не поедет никогда. Конструктор table.New принимает опции: WithColumns задаёт колонки, WithRows — данные, WithFocused — фокус при старте, WithHeight — высоту таблицы, WithWidth — ширину. Курсор — это не координаты на экране, а индекс выбранной строки: метод Cursor возвращает его, SetCursor ставит с зажимом в допустимые границы. Отсюда простая привычка: думать о таблице как о данных с индексом, а не как о разметке.

Модель с таблицей задач

GO
type taskModel struct {
        table table.Model
}

func initialTaskModel() taskModel {
        cols := []table.Column{
                {Title: "Задача", Width: 24},
                {Title: "Ветка", Width: 12},
                {Title: "Статус", Width: 10},
        }
        rows := []table.Row{
                {"почистить кэш", "main", "готово"},
                {"переиндексация", "dev", "идёт"},
        }
        t := table.New(
                table.WithColumns(cols),
                table.WithRows(rows),
                table.WithFocused(true),
                table.WithHeight(7),
        )
        return taskModel{table: t}
}

func (m taskModel) View() string {
        return m.table.View()
}

Пакет импортируется как github.com/charmbracelet/bubbles/table. Колонок три, каждая со своей шириной в символах. WithFocused(true) включает реакцию на клавиши сразу, без него таблица отрисуется, но на движение курсора не ответит. Высота семь включает строку заголовка, значит под данные остаётся шесть строк — об этом правиле дальше.

Высота, ширина и прокрутка#

Первое, что ловит всех новичков: WithHeight задаёт высоту таблицы целиком, вместе со строкой заголовка. Попросили семь — увидите шесть строк данных. Логика внутри честная: из высоты вычитается высота headersView, остаток отдаётся встроенному viewport. Если строк больше, чем влезает, прокрутка появляется сама, и курсор, дойдя до края, увлекает её за собой. Ширина работает иначе: SetWidth задаёт ширину окна, в котором живёт таблица, но колонки от этого не растягиваются — их ширина та, что вы указали в Column. Что не влезло в окно, обрезается на краю терминала, поэтому сумма Width колонок должна вписываться в ваш экран. Клавиши движения предустановлены и совпадают со списком из девятого урока: стрелки и j/k по строке, страницы и половины страницы, прыжки в начало и конец. Полный набор — в таблице ниже, а помощь можно рисовать через HelpView — метод существует, но в View он не выводится сам.

Клавиши table по умолчанию#

КлавишиДействие
up / kстрока вверх
down / jстрока вниз
b / pgupстраница вверх
f / pgdn / spaceстраница вниз
u / ctrl+uполстраницы вверх
d / ctrl+dполстраницы вниз
home / gк первой строке
end / Gк последней строке

Фокус и пересылка сообщений#

Таблица — подмодель, и всё движение клавиш живёт в одной ветке Update. Нюанс, на котором спотыкаются: без фокуса Update у таблицы игнорирует любые клавиши молча, ни ошибки, ни движения. Поэтому в примерах вокруг таблицы всегда есть ветка esc: она снимает и возвращает фокус через Blur и Focus, оставляя таблицу на экране, но отдавая клавиши остальному приложению. Второй нюанс — тип возвращаемого значения. Метод Update у table.Model возвращает пару (table.Model, tea.Cmd), а не (tea.Model, tea.Cmd), как у progress в двенадцатом уроке: там результат приходилось доставать утверждением типа, здесь присваивание прямое. Это мелочь, но именно на таких мелочах держится ощущение однородности восьмого урока: каждая библиотечная подмодель чуть-чуть своя, и шаблон пересылки копируется не вслепую, а глазами.

Выбор строки и пересылка клавиш

GO
func (m taskModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
        switch msg := msg.(type) {
        case tea.KeyMsg:
                switch msg.String() {
                case "esc":
                        if m.table.Focused() {
                                m.table.Blur()
                        } else {
                                m.table.Focus()
                        }
                case "enter":
                        return m, tea.Printf("Выбрано: %s",
                                m.table.SelectedRow()[0])
                }
        }
        var cmd tea.Cmd
        m.table, cmd = m.table.Update(msg)
        return m, cmd
}

SelectedRow возвращает строку под курсором как table.Row — обычный срез строк, отсчёт с нуля по колонкам. tea.Printf из стандартного набора команд печатает строку над интерфейсом. Все прочие клавиши падают в пересылку внизу: таблица сама разберёт j/k и страницы. Пустой var cmd перед пересылкой нужен, чтобы ветки свитча не конкурировали за область видимости.

Проверка: фокус

Таблица создана без WithFocused, пользователь жмёт j. Что произойдёт?

Проверка: высота

Сколько строк данных покажет таблица после WithHeight(7)?

Что вернёт Cursor()?

Прочитайте код и выберите, что он выведет:

rows := []table.Row{
        {"alpha"}, {"beta"}, {"gamma"}, {"delta"},
}
t := table.New(table.WithRows(rows))
t.SetCursor(100)

fmt.Println(t.Cursor())
fmt.Println(t.SelectedRow() == nil)

Обновление строк на живых данных#

Статичная таблица — редкость: обычно данные приезжают из команды — список задач из сети, результаты сканирования из воркера. Схема та же, что в двенадцатом уроке: команда возвращает сообщение, Update раскладывает его по модели. Для строк у таблицы есть пара SetRows и Rows: первый кладёт новый срез и сам обновляет внутренний viewport, второй отдаёт текущие данные, если их нужно доработать. Есть и мост из простого мира текстовых файлов — метод FromValues разбирает строку вида «a,b c,d» с указанным разделителем и превращает её в строки таблицы, удобно для CSV-выгрузок без внешних библиотек. И живая граблина: в текущей версии bubbles после SetRows курсор не поджимается под новый размер. Строк было сто, стало три, курсор остался на девяносто девятой — SelectedRow вернёт nil, и первый же выбор по enter уронит обращение по индексу. Поджимайте вручную: SetCursor(len(rows) - 1) сразу после SetRows, если строк стало меньше, чем был курсор.

Приход данных из команды

GO
type rowsMsg []table.Row

func (m taskModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
        switch msg := msg.(type) {
        case rowsMsg:
                m.table.SetRows(msg)
                if m.table.Cursor() > len(msg)-1 {
                        m.table.SetCursor(len(msg) - 1)
                }
                return m, nil
        }
        var cmd tea.Cmd
        m.table, cmd = m.table.Update(msg)
        return m, cmd
}

rowsMsg несёт готовый срез table.Row — его выгрузили из сети или собрали из файлов в команде пятого урока. SetRows обновляет отрисовку, проверка курсора страхует от выхода за границы после сжатия данных. Обратите внимание: msg приводится к срезу напрямую, поэтому передаётся в SetRows без преобразований. Прогоните ветку мысленно на пустом срезе: SetRows очистит таблицу, а len(msg)-1 станет минус единицей — SetCursor(-1) поставит курсор перед первой строкой, и SelectedRow вернёт nil при первом же enter. Пустую выгрузку лучше перехватить отдельной веткой и показать «ничего не найдено», чем кормить интерфейс мёртвым курсором.

Стили: заголовок, ячейки, выделение#

Внешний вид таблицы собран в структуре table.Styles из трёх стилей Lip Gloss — она появится в пятнадцатом уроке во всей красе, а здесь достаточно имен. Header красит строку заголовков: по умолчанию жирный текст с отступом по одному пробелу. Cell — базовый вид ячейки, те же отступы. Selected — наложение на всю выбранную строку целиком: жирный текст цветом 212 по палитре ANSI. Меняется всё через SetStyles: возьмите DefaultStyles как базу, подправьте нужное поле и отдайте обратно — таблица сразу перерисует содержимое. Именно так официальный пример снимает жирность с заголовка и красит выделение фиолетовым фоном. Здесь же есть подвох: Selected работает поверх строки, поэтому его фон перекрывает фон ячеек — цветные статусы в колонках лучше выбирать так, чтобы они читались и под выделением.

Соберите инициализацию таблицы

Восстановите конструктор: создание модели, опции колонок и строк.

func initialTaskModel() taskModel { cols := []table.Column{{Title: "Задача", Width: 24}} rows := []table.Row{{"почистить кэш", "готово"}} t := table.( table.(cols), table.(rows), table.WithFocused(true), ) return taskModel{table: t} }

Соберите сборку таблицы по шагам

Расставьте строки в порядке: данные, конструктор, размеры, возврат.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: словарь table

Термин

table.Column / table.Row

нажмите, чтобы перевернуть
Определение

Колонка — заголовок и ширина в символах; строка — срез []string по числу колонок. Значение длиннее Width обрезается с многоточием.

1 / 5

Что дальше#

Таблица не живёт одна. Выбранная строка просит подробного описания — заметки к задаче вводятся многострочным полем, и этим занимается урок про textarea: таблица и редактор вместе дают полноценную панель задач. Цвета заголовка и выделения здесь названы вскользь — урок про Lip Gloss разберёт стили терминала по-настоящему, от ANSI-кодов до рамок. В итоговом проекте таблица займёт центр экрана и будет кормиться данными из файлового воркера. Если вы знакомы с десктопным GUI по курсу Fyne, взгляните для сравнения на таблицы там — задача та же, а слов на настройку уходит заметно больше. Практика: соберите таблицу из урока, урежьте высоту до пяти, добавьте четвёртую колонку длительности и убедитесь, что при ширине терминала в восемьдесят символов она уже не влезает — почувствуйте, почему горизонтальной прокрутки просит каждый второй.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Виртуальный терминал

модель Bubble Tea · Model → Update → View

Отредактируйте модель, Update или View — и нажмите «Собрать модель»: код разберётся и начнёт отвечать на клавиши, как настоящий tea.Program.

виртуальный терминал · bubbleteaalt-screen
ОЧЕРЕДЬ СБОРОК
──────────────────────────────────────
Задача Ветка Статус
──────────────────────────────────────
▸ почистить кэш main готово
переиндексацияdev идёт
деплой на продrelease в очереди
↑/↓ — курсор, enter — выбрать, n — добавить строку, q — выход

Клавиатура — обработано в Update

Журнал программы

модель собрана: names []string (3) · branches []string (3) · statuses []string (3) · cursor int = 0 · selected string

программа работает — нажмите клавишу ниже

Коротко: что запомнить

  • table.Model — колонки из table.Column, строки из table.Row, конструктор table.New с опциями.
  • Высота включает строку заголовка: WithHeight(7) оставляет шесть строк данных.
  • Без фокуса таблица молча игнорирует клавиши; Update пересылается прямым присваиванием без assertion.
  • SetRows обновляет строки и viewport; после сжатия данных курсор поджимают вручную.
  • Стили собраны в table.Styles: Header, Cell, Selected — меняются через SetStyles.
  • Горизонтальной прокрутки нет: не влезло в ширину терминала — обрезалось краем экрана.

Домашнее задание

Соберите таблицу задач из урока: три колонки — задача, статус, длительность — и не меньше восьми строк, чтобы заработала прокрутка. Повесьте enter на печать выбранной строки через tea.Printf и esc на снятие фокуса. Затем добавьте команду, которая через три секунды присылает rowsMsg с обновлёнными статусами, и не забудьте поджать курсор, если строк стало меньше. Последним пунктом проверьте пустой ответ: пусть команда присылает пустой срез строк, а интерфейс вместо пустой сетки печатает «ничего не найдено» через tea.Printf и держит курсор на месте.

Личные заметкиПусто
Хранятся только в вашем браузере (Bubble Tea · bubbles/table: таблицы данных)

Вопросы и ответы по уроку

Колонки растягиваются на всю ширину таблицы?

Нет: ширина колонки — та, что указана в Column.Width, а SetWidth задаёт лишь ширину окна таблицы. Лишнее место остаётся пустым справа, нехватка — обрезается краем терминала. Растягивать колонки под ширину окна придётся вручную, пересчитывая Width при WindowSizeMsg.

Как узнать, какая строка выбрана, если SelectedRow вернул nil?

nil означает курсор за пределами данных — обычно после SetRows с меньшим числом строк. Проверьте Cursor() против len(m.table.Rows()) и подожмите индекс через SetCursor(len(rows)-1). В свежих версиях bubbles это поджимается автоматически, но страховка в коде стоит одной строки.

Можно ли сортировать таблицу по колонке?

Готовой сортировки в компоненте нет — это виджет отображения, а не данных. Отсортируйте срез table.Row на своей стороне (sort.Slice по нужному индексу строки) и отдайте результат в SetRows. Курсор сохранится по индексу, а не по содержимому, поэтому после сортировки может понадобиться SetCursor заново.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Сравните с альтернативами

Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.

Следующая ступень учебного пути

Zap25 уроков~9 чпрактика25 уроков уже доступны

Zap: структурное логирование на Go

Интерактивный курс Zap — быстрого логгера для Go: уровни, поля, JSON, ротация, интеграции с Gin, GORM и gRPC. 25 уроков с примерами и итоговым проектом.

  • Писать структурные логи с полями вместо строк
  • Выбирать между Logger и SugaredLogger осознанно
  • Настраивать энкодеры, выводы и ротацию файлов
  • Разбираться в zapcore: Core, Encoder, WriteSyncer, LevelEnabler