---
title: "textinput: поля ввода текста"
description: "bubbles textinput в Bubble Tea: поле ввода текста на Go — Placeholder, Focus и Blur, CharLimit и Validate, поле для пароля EchoMode, переключение полей по Tab."
course: "Bubble Tea: TUI-приложения в терминале"
url: "https://golangmaster.ru/courses/bubbletea/lessons/10-textinput/"
published: "2026-09-19"
updated: "2026-10-09"
minutes: 18
---

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

*Курс «Bubble Tea: TUI-приложения в терминале», урок 10 из 25. ~18 минут. Обновлено 09.10.2026. Источник: https://golangmaster.ru/courses/bubbletea/lessons/10-textinput/*

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

Поиск, логин, переименование файла, поле фильтра — ввод текста встречается в каждом втором 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-кода. Заметьте: проверка идёт при каждом нажатии, а не при подтверждении формы.

> **Width — это не ограничитель ввода.** Самая частая ошибка: поставить Width 10 и ждать, что длиннее десяти символов ввести нельзя. Можно — строка просто прокрутится внутри узкого окна, и Value вернёт всё, что набрали. Если нужна граница длины — CharLimit; если нужно запретить символы — Validate. Width про вёрстку, а не про правила.

Внутри поля доступен приличный набор редактирования, который вы получаете бесплатно: стрелки и привычные 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 | вставить из буфера обмена |

> **Запуск и песочница.** textinput — библиотечный компонент: в песочнице сайта его не запустить, нужен локальный Go и настоящий терминал. Соберите форму из этого урока, помигайте Tab'ом между полями и попробуйте вставить текст из буфера по ctrl+v — часть поведения живёт только в настоящем терминале.

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

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

- Вызвать метод MakePassword() после New
- ✅ Поставить pw.EchoMode = textinput.EchoPassword
- Присваивать в Value звёздочки вручную при каждом нажатии
- Парольные поля в bubbles есть только у textarea

Отображение маскирует поле EchoMode: EchoPassword рисует EchoCharacter вместо символов, EchoNone не рисует ничего. Value при этом всегда возвращает настоящую строку, поэтому проверка пароля не меняется.

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

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

- Буква появится, а ошибка запишется в поле Err
- ✅ Изменение будет отвергнуто целиком, буква не появится
- Программа упадёт с паникой
- Поле очистится полностью

Валидатор вызывается на каждый кандидат-значение; ошибка означает отказ от изменения — символ не вставляется. Ошибка сохраняется в поле Err модели, откуда её показывают пользователю.

### Что вернёт Value?

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

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

**Что выведет программа?**

- bubbletea: CharLimit не влияет на набор с клавиатуры
- ✅ bubbl: набор остановился на пятом символе
- bubble: первые шесть символов
- Пустая строка: лимит превышен и ввод отвергнут

CharLimit — жёсткий предел: символы сверх лимита не вставляются, поле останавливается на «bubbl». В отличие от Width, который ограничивает только видимость, CharLimit режет само значение — и то же самое случится при SetValue длинной строки.

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

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

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

Подсказка: Поле создаёт textinput.New, режим маскировки — константа EchoPassword, предел символов — поле CharLimit.

Правильные ответы в пропусках: `New`, `EchoPassword`, `CharLimit`

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

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

Правильный порядок строк:

1. `func newNameField() textinput.Model {`
2. `  ti := textinput.New()`
3. `  ti.Placeholder = "ваше имя"`
4. `  ti.CharLimit = 32`
5. `  ti.Focus()`
6. `  return ti`

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

- **Placeholder** — Серый текст в пустом поле — подсказка, что вводить. Исчезает при первом символе, значение не трогает.
- **Focus / Blur** — Focus активирует поле и возвращает команду мигания курсора; Blur снимает активность. Клавиши пересылает ваш корень.
- **EchoMode** — Режим отображения: EchoNormal, EchoPassword (маска EchoCharacter), EchoNone. Value всегда возвращает настоящую строку.
- **CharLimit против Width** — CharLimit режет значение; Width — сколько символов видно, окно в длинную строку. Ограничителем ввода Width не является.
- **Validate** — func(string) error, вызывается на каждое изменение. Ошибка отвергает изменение и попадает в поле Err модели.

### Что дальше

Однострочное поле — половина форм; многострочный ввод — заметки, описания, комментарии — живёт в [textarea](/courses/bubbletea/lessons/14-textarea/), родственном textinput компоненте. Тонкости Tab-навигации, Enter-подтверждение и построение экранной формы целиком — тема [урока про фокус и формы](/courses/bubbletea/lessons/20-focus-formy/). Собранная здесь форма входа почти дословно войдёт в [итоговый проект курса](/courses/bubbletea/lessons/25-itogovyj-proekt/). План всех уроков — на странице [курса Bubble Tea](/courses/bubbletea/). Практика: запустите форму локально, переключите поля Tab'ом, вставьте из буфера строку длиннее CharLimit и посмотрите на обрезку. Затем поставьте парольному полю EchoNone и почувствуйте разницу с EchoPassword — в терминале она заметнее, чем в теории. Третьим шагом попробуйте убрать Focus из Init и убедиться, что поле молчит: ошибку и её лечение стоит увидеть своими глазами.

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

- 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.

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

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

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

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

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

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

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