Поле textinput честно отслужило своё на формах входа, но у любой панели задач есть точка, где оно ломается: пользователь хочет дописать к задаче комментарий в три строки, а ввод принимает ровно одну. Пакет bubbles/textarea закрывает эту дыру: это многострочный ввод с внутренним курсором, который умеет ходить по строкам и столбцам, переносить текст по Enter, прокручивать содержимое, когда оно выше поля, и показывать номера строк. Внутри он устроен как склейка знакомых деталей: значение хранится срезом строк, отрисовка сидит на viewport из одиннадцатого урока, мигающий курсор — общий с textinput компонент cursor. Поэтому почти всё, что вы знаете о подмоделях, переносится сюда без поправок. Нового по сути два: конфигурация поля и работа с целым значением — текстом с переносами, который дальше парсится на строки.
textarea.Model: поле на много строк#
Конструктор textarea.New не принимает аргументов — в отличие от таблицы и списка с их опциями-функциями, конфигурация идёт по полям и сеттерам. Placeholder ставится присваиванием, фокус включается методом Focus, размеры — SetWidth и SetHeight. Value возвращает весь текст вместе с переносами, SetValue ставит его целиком, InsertString вставляет строку под курсором, Reset чистит поле. Фокус возвращает tea.Cmd — команду мигания курсора, и в Init уходит готовый textarea.Blink, как у textinput. Схема хранения та же, что во всех bubbles: поле ta textarea.Model в корневой модели, пересылка сообщений в Update, вызов ta.View() в общей раскладке. Отдельная приятность: поле знает свои строки и символы — LineCount возвращает число строк, Length — число символов, и эти два числа закрывают половину вопросов к заметкам.
Модель редактора заметок
GOtype noteModel struct {
ta textarea.Model
}
func initialNoteModel() noteModel {
ta := textarea.New()
ta.Placeholder = "Что нужно не забыть..."
ta.CharLimit = 500
ta.SetWidth(50)
ta.SetHeight(8)
ta.Focus()
return noteModel{ta: ta}
}
func (m noteModel) Init() tea.Cmd {
return textarea.Blink
}Это скелет официального примера textarea. Focus() вызывается в инициализации и возвращает команду мигания, но в Init всё равно уходит константа textarea.Blink — курсор начинает жить сразу. CharLimit ставится до вставок: лимит работает на всё значение, а не на одну строку. Про порядок SetWidth и полей ниже отдельный разговор.
Уберите из инициализатора ta.Focus() — и поле оживёт только внешне: Placeholder на месте, курсора нет, клавиши игнорируются. Update у textarea начинается с проверки фокуса: размытое поле молча возвращает управление, не трогая ни значение, ни курсор. Симптом тот же, что у таблицы без WithFocused из прошлого урока — интерфейс выглядит рабочим, а печатать некуда. Focus возвращает команду мигания; в примере урока её выбросили, потому что Init отдаёт константу textarea.Blink — это нормально, лишь бы сам вызов фокуса не потерялся.
Placeholder, CharLimit и размеры#
Placeholder — подсказка, которую поле рисует, пока значение пустое: серым, без курсора, до первого нажатия. CharLimit — жёсткий потолок символов на весь текст: вставка и ввод просто перестают принимать лишнее, а не падают. По умолчанию лимит равен нулю, то есть отключён, — в примерах его ставят явно, и это правильная привычка для заметок с сохранением на диск. По умолчанию поле имеет ширину сорок и высоту шесть строк; размеры меняются SetWidth и SetHeight, а MaxHeight ограничивает высоту сверху и важен, когда вы растягиваете поле под окно: больше ста строк редактор в любом случае не отдаст. Важно понимать, что высота фиксирована, а не растёт по содержимому: лишний текст скроллится внутри поля, как в viewport, курсор сам увлекает прокрутку за собой. Счётчик оставшихся символов собирается из Length и CharLimit одной строкой в статус-баре.
Статус заметки в View
GOfunc (m noteModel) View() string {
left := m.ta.View()
used := m.ta.Length()
info := fmt.Sprintf("строк: %d, символов: %d/%d",
m.ta.LineCount(), used, m.ta.CharLimit)
return left + "
" + info
}View складывает само поле и строку статистики. Length считает символы по всему значению, LineCount — строки, и оба метода читают модель, не трогая курсор. CharLimit вернётся тем, что вы поставили: рисовать его рядом с used — самый простой способ показать пользователю, сколько места осталось.
Enter, формы и многострочная вставка#
Главная ловушка компонента — Enter. У textarea по умолчанию Enter и ctrl+m вставляют перенос строки: переслали нажатие полю — получили вторую строку, а не отправку формы. Для однострочных форм решение есть в самой модели: отключить привязку m.KeyMap.InsertNewline.SetEnabled(false), тогда Enter до поля дойдёт и умрёт, а приложение обработает его как отправку. Для заметок всё наоборот: перенос нужен, и форма отправляется другой клавишей — ctrl+s в официальном примере. Вставка из буфера прилетает в Update пачкой символов и вставляется целиком: если в буфере лежит десять строк, все десять появятся в поле, а CharLimit честно обрежет хвост, который перестал влезать. С этим связано второе живое правило парсинга: пользователь вставит что угодно, включая пробелы и пустые строки, — и заметка на диске должна это пережить.
Сохранение заметки
GOtype savedMsg string
func saveNote(v string) tea.Cmd {
return func() tea.Msg {
return savedMsg(v)
}
}
func (m noteModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
if msg.String() == "ctrl+s" {
return m, saveNote(m.ta.Value())
}
}
var cmd tea.Cmd
m.ta, cmd = m.ta.Update(msg)
return m, cmd
}ctrl+s перехватывается до пересылки, поэтому в поле не попадает. Всё прочее уходит в textarea: клавиши движения, переносы, вставка. Пересылка здесь прямым присваиванием, как у таблицы из тринадцатого урока. В реальном приложении saveNote внутри команды писал бы файл, а не просто возвращал строку. Что будет, если нажать ctrl+s дважды подряд? Каждая команда уезжает в свою горутину, и две записи поедут параллельно: для крошечной заметки разница невидима, для большого файла — гонка, в которой победит последняя завершившаяся. Тяжёлое сохранение держите под флагом в модели: пока ответ не пришёл, вторую команду не запускайте.
Проверка: Enter по умолчанию
textarea в фокусе, сообщение tea.KeyMsg с Enter переслано полю без изменений. Что произойдёт?
Парсинг заметок: строки, слова, пункты#
Заметка, сохранённая целиком одной строкой в файл, обратно превращается в данные простым разбором. Value отдаёт текст с переносами; strings.Split по переводу строки режет его на строки, TrimSpace чистит крайние пробелы, фильтр по пустоте выбрасывает случайные дыры. Дальше начинается семантика вашего формата: строка, начинающаяся с дефиса и пробела, — пункт чек-листа, строка с датой — комментарий с меткой времени. Сравните LineCount и длину среза после разбиения: они почти всегда равны, но хвостовые пустые строки в разборе стоит игнорировать явно, а не надеяться на поле. Для валидации длины годится Length: заметка длиннее лимита уходит в ошибку ещё до записи на диск. Этот же разбор — мост к хранению: в итоговом проекте заметки к задачам лягут в файл рядом с самими задачами.
Что вернёт LineCount?
Прочитайте код и выберите, что он выведет:
ta := textarea.New()
ta.SetValue("первая
вторая")
fmt.Println(ta.LineCount())Лимит и SetValue
Прочитайте код и выберите, что он выведет:
ta := textarea.New()
ta.CharLimit = 10
ta.SetValue("строка номер три")
fmt.Println(ta.Length())Номера строк, промпт и внешний вид#
ShowLineNumbers включает столбец номеров слева — по умолчанию он включён, и под него резервируется четыре колонки: три цифры и отступ. Слева от каждой строки рисуется промпт — по умолчанию толстая вертикальная черта и пробел, как у редакторов из набора Charm. Цвет текущей строки, номеров и промпта собран в структуре стилей с парой FocusedStyle и BlurredStyle: поле само перекрашивается при потере фокуса, и в стилях уже спрятаны адаптивные цвета, о которых честно поговорит пятнадцатый урок. Здесь же — правило, записанное прямо в исходниках: SetWidth обязан вызываться после установки Prompt и ShowLineNumbers, потому что ширина поля считается как запрошенная минус промпт и номера строк. Поменяли внешность после SetWidth — позовите SetWidth ещё раз, иначе текст будет упираться в невидимый резерв.
Классическая ошибка: поле настраивается, SetWidth вызывается в начале, а номера строк отключаются в конце — и поле остаётся шире задуманного на четыре колонки. Запомните порядок: содержимое-конфигурация (Placeholder, Prompt, ShowLineNumbers, CharLimit), затем размеры. Тот же принцип у таблицы из прошлого урока: сначала то, что влияет на разметку, потом метры.
Главные методы и поля textarea#
| Метод / поле | Что делает |
|---|---|
| Value / SetValue | прочитать весь текст / поставить целиком, с учётом лимита |
| InsertString / Reset | вставить строку под курсором / очистить поле |
| Length / LineCount | число символов / число строк значения |
| SetWidth / SetHeight | размеры поля; SetWidth — после Prompt и номеров строк |
| CharLimit | потолок символов; 0 — без лимита (по умолчанию) |
| MaxHeight | ограничение высоты сверху, по умолчанию 99 строк |
| ShowLineNumbers | номера строк слева, по умолчанию включены |
| Focus / Blur | включить фокус (возвращает команду) / снять его |
Проверка: ширина и номера строк
Вы вызвали SetWidth(50), затем отключили ShowLineNumbers. Что нужно, чтобы ширина пересчиталась?
Соберите Init и сохранение
Восстановите команду мигания, чтение значения и пересылку сообщений.
Соберите конфигурацию поля
Расставьте строки: создание, содержимое-конфигурация, размеры, фокус.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: словарь textarea
Поле ввода живёт только в настоящем терминале: в песочнице сайта курсор и вставку из буфера не повторить. Соберите пример с ctrl+s локально, вставьте из буфера десять строк подряд и посмотрите, как CharLimit обрезает хвост, а LineCount растёт на глазах.
Что дальше#
Заметка не бывает без владельца: в уроке про таблицу выбранная строка задачи — ровно та сущность, к которой редактор прикручивается боковой панелью. Два поля рядом — уже форма, и тонкости переключения фокуса по Tab, экранных форм и кнопок разбирает урок про фокус и формы. Оба компонента — таблица и редактор — составляют ядро итогового проекта курса, где заметки сохраняются в файл и парсятся по правилам этого урока. План всей программы — на странице курса Bubble Tea. Практика: добавьте к полю счётчик оставшихся символов, отключите номера строк и следите за шириной, затем соберите разбор заметки на пункты чек-листа и выведите их списком под редактором.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Виртуальный терминал
модель Bubble Tea · Model → Update → ViewОтредактируйте модель, Update или View — и нажмите «Собрать модель»: код разберётся и начнёт отвечать на клавиши, как настоящий tea.Program.
Клавиатура — обработано в Update
модель собрана: note []string (2) · line textinput.Model · saved string
программа работает — нажмите клавишу ниже
Коротко: что запомнить
- textarea.New без аргументов: конфигурация полями и сеттерами, а не опциями конструктора.
- CharLimit режет всё значение на вводе, вставке и SetValue; по умолчанию лимита нет.
- Высота фиксирована, излишек скроллится внутри; MaxHeight ограничивает растяжение сверху.
- Enter по умолчанию вставляет перенос: для формы отключите InsertNewline или перехватите клавишу.
- SetWidth зовётся после Prompt и ShowLineNumbers — иначе резерв под номера посчитается неверно.
- Value плюс strings.Split — весь парсинг заметок: строки, фильтр пустых, пункты по префиксу.
Домашнее задание
Соберите редактор заметок: поле на восемь строк с лимитом в триста символов, счётчик строк и символов под полем и отправка по ctrl+s. Затем добавьте парсинг: после сохранения разбейте заметку на строки, выбросьте пустые и напечатайте число пунктов, начинающихся с дефиса. Проверьте на вставке в десять строк — лимит и счётчики не должны соврать. Добавьте защиту от пустой заметки: по ctrl+s проверяйте TrimSpace от значения и при пустоте печатайте подсказку через tea.Printf, не запуская сохранение.
Личные заметкиПусто
Вопросы и ответы по уроку
Поле не растёт по мере набора текста. Это так задумано?
Да: высота textarea фиксирована, а лишний текст прокручивается внутри, как в viewport. Если нужно ощущение роста, пересчитывайте SetHeight по LineCount в Update, не забывая про потолок MaxHeight. В полноэкранных приложениях высоту обычно привязывают к WindowSizeMsg, о котором будет урок про alt-screen.
Как ограничить длину отдельной строки?
Прямого ограничения на строку у компонента нет — CharLimit общий. Ставьте валидацию на своей стороне: в ветке сообщения или перед сохранением пройдитесь по Value, разбейте на строки и посчитайте длину каждой. Для заметок чаще всего хватает общего лимита и TrimSpace при разборе.
Вставка из буфера не работает в SSH-сессии. Почему?
Помимо обычной вставки терминалом, у textarea есть собственный запрос буфера обмена по ctrl+v через протокол OSC52, и его поддержка зависит от терминала. В большинстве случаев достаточно обычной вставки комбинацией терминала: текст придёт пачкой символов и попадёт в поле целиком, с учётом CharLimit.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
Следующая ступень учебного пути
Zap: структурное логирование на Go
Интерактивный курс Zap — быстрого логгера для Go: уровни, поля, JSON, ротация, интеграции с Gin, GORM и gRPC. 25 уроков с примерами и итоговым проектом.
- Писать структурные логи с полями вместо строк
- Выбирать между Logger и SugaredLogger осознанно
- Настраивать энкодеры, выводы и ротацию файлов
- Разбираться в zapcore: Core, Encoder, WriteSyncer, LevelEnabler