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

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

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

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

textinput: поля ввода текста

Поля ввода — самый частый элемент интерфейса после списков, и в Bubble Tea для них есть готовый пакет bubbles/textinput: однострочное поле с курсором, заполнителем и валидацией. Вручную такой элемент не пишут: забой по рунам, вставка из буфера, мигающий курсор — это сотни строк. Разбираем Placeholder, Focus и Blur, лимиты, парольный режим EchoMode и переключение нескольких полей по Tab.

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

Поиск, логин, переименование файла, поле фильтра — ввод текста встречается в каждом втором TUI. И это как раз тот случай, где самодельное решение ломается первым: в седьмом уроке курса мы уже видели, что клавиши в bubbletea приходят строками, но превратить их в редактируемый текст — отдельная работа. Нужно двигать курсор по рунам, а не по байтам, обрабатывать backspace на границе UTF-8, принимать вставку из буфера обмена, показывать мигание, не дать стрелкам уехать за края. Каждая деталь — источник тихих багов с кириллицей: байт и рун в русском слове не совпадают, и самодельный забой по байтам режет буквы пополам. Пакет github.com/charmbracelet/bubbles/textinput закрывает всё перечисленное одним типом: textinput.Model — такая же подмодель, как список из прошлого урока, со своим Update, View и полем состояния. Вы храните его в своей модели, пересылаете клавиши и читаете итог методом Value. Сегодня соберём форму входа из двух полей и разберём настройки, которые чаще всего путают.

textinput.Model: поле как подмодель#

textinput.New() возвращает модель с настройками по умолчанию: подсказка-приглашение "> ", без лимита длины. Поле Placeholder — серый текст внутри пустого поля, который исчезает при первом символе: именно им принято объяснять, что вводить. Prompt — то, что рисуется слева от текста, стандартно "> ", его часто заменяют на пустую строку или на метку вроде "имя: ". View рисует либо заполнитель, либо значение с курсором в нужной позиции. Курсор мигает сам — библиотека генерирует сообщения мигания, и вам о них думать не нужно, но команда на запуск мигания возвращается методом Focus, поэтому её стоит возвращать из Update, а не выбрасывать. Значение читается методом Value, устанавливается SetValue, сбрасывается Reset. Схема встраивания дословно та же, что у list: поле в структуре, пересылка сообщений, View в кадр.

Минимальное поле

GO
type nameModel struct {
        login textinput.Model
}

func initialNameModel() nameModel {
        li := textinput.New()
        li.Placeholder = "ваше имя"
        li.CharLimit = 32
        li.Focus()
        return nameModel{login: li}
}

func (m nameModel) View() string {
        return m.login.View()
}

Создание поля — три строки: New, Placeholder, лимит. Focus перед первым кадром обязателен: несфокусированное поле не отвечает на клавиши и не показывает курсор. Метод Focus возвращает tea.Cmd для запуска мигания — в Init его стоит вернуть программе. CharLimit здесь — мягкая страховка, чтобы имя не разъехалось на весь экран.

Focus и Blur: кто слушает клавиатуру#

Фокус у поля — это разрешение на клавиши и видимый мигающий курсор. Focus переводит поле в активное состояние и возвращает команду запуска мигания, Blur снимает: поле замирает, курсор исчезает, нажатия больше не доходят — если, конечно, вы их не пересылаете. Последняя оговорка важна: фокус в bubbles — это соглашение, а не канал доставки. Клавиши идут через ваш корневой Update, и пересылать их нужно только активному полю — ровно та маршрутизация по фокусу, которую мы разобрали на подмоделях в восьмом уроке. Если переслать KeyMsg обоим полям, буквы появятся в двух местах сразу. Отсюда практическое правило: поле ввода само не ворует клавиатуру — у него нет для этого механизма, и это хорошо, потому что решение о доставке остаётся у корня. Проверить состояние можно методом Focused, но обычно вы и так знаете активное поле из собственной переменной фокуса. И не забывайте команду мигания: сфокусированное поле, чью Focus-команду вы выбросили, будет рисовать курсор статично, пока очередное Focus не запустит мигание заново.

Два поля и переключение по Tab

GO
type loginModel struct {
        login    textinput.Model
        password textinput.Model
        focused  int // 0 — логин, 1 — пароль
}

func (m loginModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
        if key, ok := msg.(tea.KeyMsg); ok {
                switch key.String() {
                case "tab":
                        if m.focused == 0 {
                                m.focused = 1
                                m.login.Blur()
                                return m, m.password.Focus()
                        }
                        m.focused = 0
                        m.password.Blur()
                        return m, m.login.Focus()
                case "enter":
                        return m, tea.Quit
                }
        }
        var cmd tea.Cmd
        if m.focused == 0 {
                m.login, cmd = m.login.Update(msg)
        } else {
                m.password, cmd = m.password.Update(msg)
        }
        return m, cmd
}

Tab перехватывает корень: снимает фокус с текущего поля и передаёт следующему, возвращая команду мигания. Остальные клавиши уходят строго активному полю — второе нажатий не видит. Заметьте, что переменная фокуса — простое целое: для трёх полей она тоже останется числом, а вот для растущей формы из двадцати полей уже просится срез textinput.Model и индекс вместо отдельных полей. Enter здесь завершает программу; в настоящем приложении на этом месте читают значения через Value и проверяют форму.

Парольное поле — тот случай, ради которого textinput берут даже в учебные проекты. За отображение отвечает поле EchoMode: EchoNormal показывает символы как есть, EchoPassword заменяет каждый на EchoCharacter — по умолчанию звёздочку, часто ставят точку-буллит, EchoNone не рисует ничего, как в sudo. Важно понимать, что режим влияет только на отрисовку: Value возвращает настоящую строку в любом случае, так что проверка пароля работает как обычно. Смена EchoCharacter — один символ в кавычках, но помните про ширину: маскирующие символы занимают ровно одну ячейку, и поле не «дышит» при наборе. Отдельный аргумент за EchoPassword — журналы: скриншот с настоящим паролем живёт дольше, чем сессия терминала, а точечки хотя бы честно показывают, что секрет был введён. Для пароля почти всегда ставят CharLimit — не из паранойи, а потому что тридцатисимвольный пароль, уехавший за край узкого поля, невозможно ввести осознанно: Width рисует окно в текст, и длинная строка прокручивается внутри него.

Поле пароля

GO
pw := textinput.New()
pw.Placeholder = "пароль"
pw.EchoMode = textinput.EchoPassword
pw.EchoCharacter = '•'
pw.CharLimit = 64
pw.Width = 20

EchoPassword маскирует ввод символом EchoCharacter — здесь буллитом. Width задаёт видимую ширину в символах: сама строка может быть длиннее, поле прокрутит её. CharLimit — честный предел длины: символы сверх шестидесяти четырёх просто не вставятся, в отличие от Width, который ограничивает только картинку.

CharLimit, Width и Validate: три разных ограничения#

Здесь путаются чаще всего, поэтому разложим аккуратно. CharLimit — жёсткий предел количества символов: ноль означает «без лимита», любое положительное число обрезает и набор с клавиатуры, и программную установку SetValue. Width — сколько символов видно одновременно: это окно в длинную строку, а не ограничитель, значение продолжает накапливаться и прокручиваться. Validate — функция func(string) error, которую библиотека зовёт при каждом изменении значения: вернули ошибку — изменение отвергается целиком, символ не появится, а ошибка остаётся в поле Err модели, откуда её можно показать рядом красным. Это валидация в момент набора, а не по кнопке: буква в числовом поле не успевает промелькнуть. Комбинация на практике простая: CharLimit — для страховки длины, Width — для вёрстки, Validate — для содержимого. Все три настройки независимы и применяются вместе: PIN-код из урока ниже держится сразу на двух из них.

Поле только для цифр

GO
pin := textinput.New()
pin.Placeholder = "1234"
pin.CharLimit = 4
pin.Validate = func(s string) error {
        for _, r := range s {
                if r < '0' || r > '9' {
                        return errors.New("только цифры")
                }
        }
        return nil
}

Валидатор получает кандидат на новое значение целиком и решает, пускать ли его. Нажатие буквы вернёт ошибку — изменение не применится, Err примет ошибку, и её можно нарисовать рядом. CharLimit 4 страхует длину PIN-кода. Заметьте: проверка идёт при каждом нажатии, а не при подтверждении формы.

Внутри поля доступен приличный набор редактирования, который вы получаете бесплатно: стрелки и привычные emacs-сочетания двигают курсор, ctrl+a и home — в начало, ctrl+e и end — в конец, alt+стрелки прыгают по словам. Удаление: backspace стирает слева, delete — справа, ctrl+w удаляет слово слева, ctrl+u очищает всё до курсора, ctrl+k — всё после. Вставка из буфера приходит по ctrl+v — честно работает в настоящем терминале, поддерживающем bracketed paste, и ей не нужна ваша обработка. Для программной работы есть CursorStart, CursorEnd и SetCursor для позиционирования. Всё это стоит проверить руками хотя бы раз: понимание, что поле уже умеет, спасает от дублирования библиотеки собственным кодом — классическая трата вечера, которую я наблюдал не раз. В свежих версиях bubbles у поля есть и автодополнение: SetSuggestions задаёт список вариантов, стрелки вверх-вниз листают подсказку — для имён команд и путей это экономит и поле, и терпение пользователя.

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

КлавишаДействие
← / →, ctrl+b / ctrl+fсдвинуть курсор на символ
ctrl+a / home, ctrl+e / endв начало и в конец строки
alt+← / alt+→прыжок по словам
ctrl+wудалить слово слева от курсора
ctrl+u, ctrl+kудалить всё до курсора / после курсора
ctrl+vвставить из буфера обмена

Проверка: парольный режим

Как сделать из обычного поля поле для пароля?

Проверка: валидация

Validate вернула ошибку на попытке вставить букву в числовое поле. Что произойдёт?

Что вернёт Value?

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

ti := textinput.New()
ti.CharLimit = 5

// Пользователь набрал: bubbletea
// ti.Value() == ?

Заполните создание поля пароля

Восстановите конструктор, парольный режим и лимит длины.

func newPassword() textinput.Model { pw := textinput. pw.Placeholder = "пароль" pw.EchoMode = textinput. pw. = 64 pw.Focus() return pw }

Соберите фабрику поля

Расставьте строки функции, возвращающей готовое сфокусированное поле имени.

Строки

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

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

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

Термин

Placeholder

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

Серый текст в пустом поле — подсказка, что вводить. Исчезает при первом символе, значение не трогает.

1 / 5

Что дальше#

Однострочное поле — половина форм; многострочный ввод — заметки, описания, комментарии — живёт в textarea, родственном textinput компоненте. Тонкости Tab-навигации, Enter-подтверждение и построение экранной формы целиком — тема урока про фокус и формы. Собранная здесь форма входа почти дословно войдёт в итоговый проект курса. План всех уроков — на странице курса Bubble Tea. Практика: запустите форму локально, переключите поля Tab'ом, вставьте из буфера строку длиннее CharLimit и посмотрите на обрезку. Затем поставьте парольному полю EchoNone и почувствуйте разницу с EchoPassword — в терминале она заметнее, чем в теории. Третьим шагом попробуйте убрать Focus из Init и убедиться, что поле молчит: ошибку и её лечение стоит увидеть своими глазами.

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

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

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

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

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

виртуальный терминал · bubbleteaalt-screen
textinput: Placeholder «ваше имя», CharLimit 32
логин >
EchoMode: EchoPassword
введите текст в поле ниже — эмуляция набора; e — EchoMode, enter — Value()

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

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

модель собрана: login textinput.Model · hidden bool = false · status string = "EchoPassword"

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

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

  • textinput.Model — готовое однострочное поле: курсор, забой, вставка и мигание уже внутри.
  • Focus активирует поле и возвращает команду мигания; Blur снимает; клавиши пересылает корень по фокусу.
  • EchoMode маскирует отображение: EchoPassword с EchoCharacter, EchoNone для sudo-тишины; Value честный.
  • CharLimit режет значение, Width задаёт видимое окно, Validate func(string) error отвергает недопустимый ввод.
  • Поле — подмодель: храните полем, пересылайте сообщения, забирайте View и Value.
  • Несколько полей — переменная фокуса в корне и Tab, который обрабатывает корневой Update.

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

Соберите форму из двух полей: логин с CharLimit 32 и пароль с EchoMode EchoPassword. Настройте переключение по Tab и вывод обоих значений по Enter через fmt.Println перед выходом. Затем добавьте третье поле с валидатором только на цифры и убедитесь, что буква в нём не появляется, а ошибка доступна в поле Err.

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

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

Почему поле не реагирует на клавиши?

Чаще всего поле просто не сфокусировано: без Focus() оно не показывает курсор и не обрабатывает ввод. Вторая причина — клавиши не пересылаются полю: textinput получает сообщения только через вызов его Update в вашем корневом Update. Проверьте обе точки по очереди.

Как показать ошибку валидации пользователю?

После неудачного изменения поле Err модели содержит ошибку, которую вернул валидатор. Нарисуйте её под полем в View — красным через Lip Gloss. Очистите по следующему успешному изменению или вручную, иначе ошибка будет висеть вечно.

Можно ли вводить многострочный текст?

Нет, textinput — строго однострочное поле: Enter по умолчанию к нему не привязан и остаётся вашей клавишей подтверждения. Для заметок и описаний используйте textarea из того же набора bubbles — у неё есть свой скроллинг, лимиты и высота.

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

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

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

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

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

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

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

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

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