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

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

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

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

Клавиатура: KeyMsg и обработка нажатий

В bubbletea клавиши — это обычные сообщения: каждое нажатие, от стрелки до ctrl+c, приходит в Update значением tea.KeyMsg, а решает, что с ним делать, ваш switch. Разберём строки клавиш, модификаторы, ловушки вроде пробела-из-одного-пробела и связку key.Binding с key.Matches для больших программ.

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

Пальцы опытного разработчика сами помнят: 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: навигация и выход

GO
func (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 с подписями для справки

GO
import "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 и в каком порядке. Держите такую отладку под рукой при работе с раскладками и модификаторами: терминальные сюрпризы, о которых шла речь выше, невозможно вычитать из документации — их видно только на живых нажатиях. Уберите отладочную печать перед выпуском: пользователь не должен видеть поток ваших сообщений — это внутренность, а не интерфейс.

Типичная ошибка: программа не выходит#

Самый частый вопрос новичков на форумах: «скопировал программу, работает, но 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

Заполните пропуски в разборе клавиш

Восстановите тип-свитч: тип сообщения, метод имени клавиши, клавишу вниз и команду выхода.

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.: switch msg.() { case "": m.cursor++ case "ctrl+c": return m, tea. } } return m, nil }

Соберите проверку через key.Matches

Расставьте строки Update со связкой key.Binding в правильном порядке.

Строки

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

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

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

Термин

tea.KeyMsg

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

Сообщение о нажатии клавиши: поле Type, руны Runes, флаг Alt. В bubbletea keymsg приходит в Update на каждое нажатие.

1 / 6

Что дальше#

Клавиши — не единственные гости Update. Пятый урок про tea.Cmd добавит в свитч собственные сообщения от фоновой работы: ответы сети, тики, результаты файлов — клавиатура станет лишь одним из источников событий. Готовые компоненты снимут с вас и клавиатуру тоже: bubbles/list уже умеет j/k, фильтр и справку, а к итоговому проекту мы соберём панель задач со своей связной схемой клавиш по всем правилам этого урока. Домашняя практика напрашивается сама: возьмите счётчик, добавьте vim-синонимы и ветку ctrl+c, затем проверьте его в русской раскладке — q перестанет работать, и вы своими глазами увидите, почему чек-лист выше требует стрелок для всего важного. Запускать, как всегда, локально: терминал, Go, пять минут.

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

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

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

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

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

виртуальный терминал · bubbleteaalt-screen
Клавиша: — нажатий: 0
стрелки — пробовать, q — выход

Клавиатура — обработано в 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) в лог, и выпишите, что реально приходит на десять разных нажатий.

Личные заметкиПусто
Хранятся только в вашем браузере (Bubble Tea · Клавиатура: KeyMsg и обработка нажатий)

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

Как обработать одновременно нажатие двух клавиш?

Обычному TUI это не нужно: клавиши приходят последовательно, и сложные действия выражают последовательностями или префиксами, как в vim. Полноценные мультинажатия терминал в общем случае не передаёт — это ограничение платформы, а не Bubble Tea.

Почему ctrl+c не завершает программу автоматически?

В сыром режиме терминал не посылает сигнал: ctrl+c приходит обычным KeyMsg. Фреймворк оставляет решение за вами — где-то выход нежелателен без сохранения. Верните tea.Quit из ветки "ctrl+c" — и выход заработает.

Чем key.Binding лучше обычного switch по строкам?

Binding хранит синонимы и подписи в одном месте: справка по клавишам строится из тех же значений, что и проверка в Update. Для маленьких программ это лишний синтаксис; на нескольких экранах он экономит больше времени, чем стоит.

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

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

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

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

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

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

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

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

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