Содержание урока · 12 разделов
- tea.KeyMsg: как терминал отдаёт клавиши
- Словарь клавиш: что вернёт msg.String()
- Модификаторы: ctrl, alt и что различить нельзя
- key.Binding и key.Matches: когда клавиш много
- Отладка ввода: увидеть, что реально пришло
- Типичная ошибка: программа не выходит
- Как выбирать клавиши для своего приложения
- Чек-лист клавиатурной схемы
- Что дальше
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Пальцы опытного разработчика сами помнят: j — вниз, k — вверх, q — выход. Программа, которая не читает клавиатуру, — это демо, а инструментом её не назовёшь: вся ценность TUI живёт в реакциях на нажатия. До сих пор мы обрабатывали клавиши по остаточному принципу — два case в счётчике, и хватало. Сегодня разберём ввод по-настоящему: как терминал отдаёт нажатия, почему ctrl+c не выходит из программы сам, какие имена у клавиш и что делать, когда case'ов становится двадцать. Код урока, как всегда, рассчитан на локальный запуск — полный разбор работает только в настоящем терминале, и это честнее любых скриншотов.
tea.KeyMsg: как терминал отдаёт клавиши#
Bubble Tea переводит терминал в сырой режим: клавиши перестают порождать сигналы и печатать буквы, вместо этого каждая нажатая кнопка читается как байтовая последовательность. Фреймворк собирает эти байты в сообщение tea.KeyMsg — в bubbletea keymsg устроен просто: поле Type с кодом клавиши, поле Runes с набранными символами и поле Alt. Обращаться к полям можно, но в девяноста девяти процентах случаев хватает одной строки — msg.String(): она возвращает каноническое имя нажатия, по которому удобно свитчиться. tea.KeyUp.String() даёт "up", ctrl+c даёт "ctrl+c", буква k даёт "k". Всё сообщение обрабатывается в привычном тип-свитче: case tea.KeyMsg перехватывает нажатия, а вложенный switch по msg.String() разбирает, что именно нажали. Неперехваченные клавиши уходят в финальный return m, nil — программа не падает и не жужжит, просто игнорирует. Для полей ввода пригодится и поле Runes — набор набранных символов: им читают пользовательский текст, когда обработка отдельных клавиш не нужна. Начнём с типового Update, который покрывает девять десятых интерфейсов.
Типовой Update: навигация и выход
GOfunc (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "up", "k":
if m.cursor > 0 {
m.cursor--
}
case "down", "j":
if m.cursor < len(m.items)-1 {
m.cursor++
}
case " ":
m.paused = !m.paused
case "q", "esc":
return m, tea.Quit
case "ctrl+c":
return m, tea.Quit
}
}
return m, nil
}Внешний свитч фильтрует только нажатия, внутренний — разбирает по именам. В одном case перечисляют синонимы: "up", "k" — стрелки и vim-стиль для одного действия. Движение курсора защищено проверками границ: без них курсор уйдёт за список молча и навсегда. Пробел переключает паузу — его имя строка из одного пробела. q и esc — выход, ctrl+c — тоже выход, но его обязательно перехватывать, и вот почему: в сыром режиме ctrl+c не посылает сигнал процессу, он приходит как обычное KeyMsg, и если вы его не обработали, программа продолжит жить.
Словарь клавиш: что вернёт msg.String()#
| Клавиша | msg.String() | Заметка |
|---|---|---|
| ↑ | "up" | стрелки: up, down, left, right |
| Enter | "enter" | подтверждение, выбор |
| Esc | "esc" | отмена, выход без сохранения |
| Tab | "tab" | переход к следующему полю; назад — "shift+tab" |
| Пробел | " " | ловушка: имя — строка из одного пробела |
| Буква A | "a" | обычные буквы приходят как есть, в нижнем регистре |
| Shift+A | "A" | заглавная буква — просто другой символ |
| Ctrl+C | "ctrl+c" | не сигнал, а сообщение: выход только если обработали |
| Alt+S | "alt+s" | модификатор alt добавляет префикс alt+ |
| Backspace | "backspace" | для полей ввода: delete, home, end, pgup, pgdown тоже именованы |
Модификаторы: ctrl, alt и что различить нельзя#
Модификаторы — территория, где терминальная платформа напоминает о себе. Ctrl с буквами работает предсказуемо: "ctrl+a", "ctrl+c", "ctrl+d" — имена строятся единообразно. Alt добавляет префикс: нажатие alt+↑ даст "alt+up", alt+s — "alt+s"; то же самое видно и в поле Alt сообщения — булево значение, которое можно проверять напрямую. А дальше начинается честная оговорка. Shift с буквой не даст "shift+a" — придёт просто "A", заглавная буква. Некоторые комбинации неразличимы в принципе: ctrl+i исторически посылает тот же байт, что tab, ctrl+m — что enter; сколько ни свитчься, различить их нельзя. И это не упрёк Bubble Tea — фреймворк лишь читает то, что до него дошло: эмуляторы терминалов сорок лет назад договорились о байтовых кодах, и часть информации в этих кодах потеряна навсегда. Практический вывод: проектируйте схемы клавиш из того, что гарантированно различимо — стрелки, буквы, цифры, ctrl с буквами, esc, enter, tab. Всё остальное — приятный бонус там, где он работает, но не опора архитектуры: интерфейс, который ломается в половине эмуляторов, хуже интерфейса без этих клавиш вообще.
Проверка: имя пробела
Что вернёт msg.String() при нажатии клавиши пробела?
Проверка: выход по ctrl+c
Программа не выходит по ctrl+c. В чём причина?
Проверка: модификаторы
Пользователь нажал Alt и стрелку вверх. Что вернёт msg.String()?
key.Binding и key.Matches: когда клавиш много#
Свитч по строкам прекрасно работает до десяти-пятнадцати клавиш. Дальше начинается жизнь: схемы клавиш умножаются на экраны, появляется справка по клавишам, а строка "up", "k" повторяется в трёх файлах. Для этого случая в bubbles есть пакет key: каждая клавиша — значение key.Binding с набором альтернативных кнопок и подписью для справки, а проверка идёт функцией key.Matches. Binding собирают в отдельную структуру keyMap — она превращается в единственное место, где клавиши перечислены, и одновременно в источник данных для справочного экрана: подпись уже написана, осталось отрисовать. Скажу честно своё спорное мнение: для маленьких программ связка Binding+Matches избыточна — плоский switch читается быстрее и без синтаксиса. Оправдание приходит с масштабом: несколько экранов, справка по клавишам, желание переопределять схемы. Тогда Binding платит за себя с процентами.
keyMap с подписями для справки
GOimport "github.com/charmbracelet/bubbles/key"
type keyMap struct {
Up key.Binding
Down key.Binding
Quit key.Binding
}
var keys = keyMap{
Up: key.NewBinding(key.WithKeys("up", "k"), key.WithHelp("↑/k", "вверх")),
Down: key.NewBinding(key.WithKeys("down", "j"), key.WithHelp("↓/j", "вниз")),
Quit: key.NewBinding(key.WithKeys("q", "ctrl+c"), key.WithHelp("q", "выход")),
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch {
case key.Matches(msg, keys.Up):
m.cursor--
case key.Matches(msg, keys.Down):
m.cursor++
case key.Matches(msg, keys.Quit):
return m, tea.Quit
}
return m, nil
}keyMap — структура с полем на каждое действие программы. key.NewBinding описывает клавиши: WithKeys перечисляет строки-синонимы, WithHelp добавляет подпись для справки. key.Matches проверяет сообщение против binding — внутри это тот же type assertion на KeyMsg и сравнение строк, просто в читаемой обёртке. Свитч по truecase заменяет вложенные свитчи. В Update кнопки теперь живут только в keys — переопределить схему можно в одном месте.
Отладка ввода: увидеть, что реально пришло#
Схема клавиш редко работает с первого раза, поэтому отладка ввода — навык из обязательного набора. Самый прямой способ — временная ветка в Update, которая печатает каждое нажатие в лог: string(msg) даёт ровно ту строку, по которой вы свитчитесь, и опечатки вроде «ctr+c» становятся видны мгновенно. В самой Bubble Tea есть и штатный инструмент — команда tea.Println: она печатает строку над кадром View, не ломая отрисовку, и удобно показывает, какие сообщения доходят до Update и в каком порядке. Держите такую отладку под рукой при работе с раскладками и модификаторами: терминальные сюрпризы, о которых шла речь выше, невозможно вычитать из документации — их видно только на живых нажатиях. Уберите отладочную печать перед выпуском: пользователь не должен видеть поток ваших сообщений — это внутренность, а не интерфейс.
Bubble Tea доставляет символы так, как их набрали: в русской раскладке клавиша «й» придёт строкой "й", а не "q". Схема «стрелки для всех, латинские буквы для vim-привычек» работает, а вот надежда «пользователь же переключит раскладку» — нет. Для моноязычных инструментов это боль известная; проверяйте раскладку перед тем, как обещать горячие клавиши.
Типичная ошибка: программа не выходит#
Самый частый вопрос новичков на форумах: «скопировал программу, работает, но ctrl+c не помогает — что я делаю не так?» Ответ выше, повторить его стоит отдельным абзацем, потому что привычка «ctrl+c убивает всё» тащится из обычного терминала и там не работает. Bubble Tea в сыром режиме получает ctrl+c как обычное сообщение, и программа живёт ровно до тех пор, пока вы сами не вернёте tea.Quit. Вторая ошибка рядом: ждать реакции на клавишу, имя которой написано с опечаткой — case "ctr+c" компилируется без замечаний и просто никогда не сработает. Лекарство одно и то же: сверяйте имена с таблицей из этого урока, а неперехваченные нажатия во время отладки печатайте в лог — string(msg) в default-ветке покажет, что реально приходит. Это то место, где полные программы стоит гонять локально: настоящий терминал и ваша рука на клавиатуре покажут правду быстрее любых логов. И ещё пара слов про рефлекс: привычка «ctrl+c жмётся без вопросов» тащится из десяти лет работы в шелле, и именно она вызывает этот вопрос чаще всего — переучить себя дешевле, чем отлаживать молчащий терминал.
Как выбирать клавиши для своего приложения#
Схема клавиш — это интерфейс, и проектировать её стоит как интерфейс. Стрелки — обязательный минимум: они работают в любой раскладке и не требуют объяснений. vim-синонимы j/k/h/l — подарок привыкшим, они никогда не мешают, пока стрелки остаются. Выход дублируйте: q для здравомыслящих, ctrl+c для рефлекса, esc для паники — три пути лучше одного. Опасные действия не вешайте на одиночные буквы: enter с подтверждением или буква с заглавной (сдвиг) защитят от случайностей. И главное — клавиши должны быть видимы: подпись из key.WithHelp обязана дойти до экрана, иначе схема живёт только в вашей голове. У готовых компонентов bubbles эти правила уже встроены: список из девятого урока приносит с собой и навигацию, и фильтр, и справку — иногда честнее взять готовое, чем изобретать четвёртую свою схему. Правило последнее: изменили схему — обновите справку в том же коммите, рассинхрон клавиш и подсказок раздражает сильнее отсутствия подсказок.
Чек-лист клавиатурной схемы#
- Стрелки работают всегда и для всех — это база, от которой не отступают
- Вим-синонимы j/k добавлены, но не заменяют стрелки
- Выход дублируется: q, ctrl+c и esc ведут к одному результату
- Границы движения проверены: курсор не уходит за пределы списка молча
- Пробел написан как " ", а не "space" — проверено на живой программе
- Все клавиши подписаны через key.WithHelp и видны на справочном экране
Куда двинется курсор?
Прочитайте код и выберите, что он выведет:
type model struct {
items []string
cursor int
}
func (m model) View() string {
return fmt.Sprintf("Позиция: %d\n", m.cursor)
}
// m.items содержит 3 элемента, m.cursor = 0.
// Пользователь нажал: k, ↑, j, j, jЗаполните пропуски в разборе клавиш
Восстановите тип-свитч: тип сообщения, метод имени клавиши, клавишу вниз и команду выхода.
Соберите проверку через key.Matches
Расставьте строки Update со связкой key.Binding в правильном порядке.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: словарь клавиатуры
Что дальше#
Клавиши — не единственные гости Update. Пятый урок про tea.Cmd добавит в свитч собственные сообщения от фоновой работы: ответы сети, тики, результаты файлов — клавиатура станет лишь одним из источников событий. Готовые компоненты снимут с вас и клавиатуру тоже: bubbles/list уже умеет j/k, фильтр и справку, а к итоговому проекту мы соберём панель задач со своей связной схемой клавиш по всем правилам этого урока. Домашняя практика напрашивается сама: возьмите счётчик, добавьте vim-синонимы и ветку ctrl+c, затем проверьте его в русской раскладке — q перестанет работать, и вы своими глазами увидите, почему чек-лист выше требует стрелок для всего важного. Запускать, как всегда, локально: терминал, Go, пять минут.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Виртуальный терминал
модель Bubble Tea · Model → Update → ViewОтредактируйте модель, Update или View — и нажмите «Собрать модель»: код разберётся и начнёт отвечать на клавиши, как настоящий tea.Program.
Клавиатура — обработано в Update
модель собрана: last string = "—" · presses int = 0
программа работает — нажмите клавишу ниже
Коротко: что запомнить
- Каждое нажатие приходит в Update как tea.KeyMsg; каноническое имя даёт msg.String().
- Имена: "up", "down", "enter", "esc", "tab", "ctrl+c", "alt+up"; пробел — строка из одного пробела.
- Ctrl+C в сыром режиме — не сигнал, а сообщение: выход только через собственную ветку с tea.Quit.
- Alt даёт префикс alt+; shift с буквой приходит заглавной буквой; ctrl+i и tab неразличимы — так устроены терминалы.
- key.Binding и key.Matches централизуют схему клавиш и кормят справку — оправдано на нескольких экранах.
- Русская раскладка шлёт "й", а не "q": важные действия дублируйте стрелками.
- Неперехваченные клавиши молча игнорируются: сверяйте имена с таблицей и печатайте их в лог при отладке.
Домашнее задание
Расширьте счётчик из второго урока: добавьте vim-синонимы j и k для движения, паузу на пробел с выводом статуса и выход по q, esc и ctrl+c одновременно. Затем запустите программу в русской раскладке и проверьте, какие клавиши работают без переключения. В конце добавьте default-ветку, печатающую string(msg) в лог, и выпишите, что реально приходит на десять разных нажатий.
Личные заметкиПусто
Вопросы и ответы по уроку
Как обработать одновременно нажатие двух клавиш?
Обычному TUI это не нужно: клавиши приходят последовательно, и сложные действия выражают последовательностями или префиксами, как в vim. Полноценные мультинажатия терминал в общем случае не передаёт — это ограничение платформы, а не Bubble Tea.
Почему ctrl+c не завершает программу автоматически?
В сыром режиме терминал не посылает сигнал: ctrl+c приходит обычным KeyMsg. Фреймворк оставляет решение за вами — где-то выход нежелателен без сохранения. Верните tea.Quit из ветки "ctrl+c" — и выход заработает.
Чем key.Binding лучше обычного switch по строкам?
Binding хранит синонимы и подписи в одном месте: справка по клавишам строится из тех же значений, что и проверка в Update. Для маленьких программ это лишний синтаксис; на нескольких экранах он экономит больше времени, чем стоит.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
Следующая ступень учебного пути
Zap: структурное логирование на Go
Интерактивный курс Zap — быстрого логгера для Go: уровни, поля, JSON, ротация, интеграции с Gin, GORM и gRPC. 25 уроков с примерами и итоговым проектом.
- Писать структурные логи с полями вместо строк
- Выбирать между Logger и SugaredLogger осознанно
- Настраивать энкодеры, выводы и ротацию файлов
- Разбираться в zapcore: Core, Encoder, WriteSyncer, LevelEnabler