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

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

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

Урок 14 из 25 ~16 мин +50 XP

textarea: многострочный ввод

Описание бага, заметка к задаче, черновик коммит-сообщения — всё, что не помещается в одну строку, вводит bubbles/textarea: многострочный ввод в go-приложении на Bubble Tea, родственник поля textinput из десятого урока. Внутри — настоящий маленький редактор: курсор, перенос строк, прокрутка и номера строк. Разбираем настройку, лимиты, вставку из буфера и превращение заметки в данные.

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

Поле 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 — число символов, и эти два числа закрывают половину вопросов к заметкам.

Модель редактора заметок

GO
type 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 и полей ниже отдельный разговор.

Placeholder, CharLimit и размеры#

Placeholder — подсказка, которую поле рисует, пока значение пустое: серым, без курсора, до первого нажатия. CharLimit — жёсткий потолок символов на весь текст: вставка и ввод просто перестают принимать лишнее, а не падают. По умолчанию лимит равен нулю, то есть отключён, — в примерах его ставят явно, и это правильная привычка для заметок с сохранением на диск. По умолчанию поле имеет ширину сорок и высоту шесть строк; размеры меняются SetWidth и SetHeight, а MaxHeight ограничивает высоту сверху и важен, когда вы растягиваете поле под окно: больше ста строк редактор в любом случае не отдаст. Важно понимать, что высота фиксирована, а не растёт по содержимому: лишний текст скроллится внутри поля, как в viewport, курсор сам увлекает прокрутку за собой. Счётчик оставшихся символов собирается из Length и CharLimit одной строкой в статус-баре.

Статус заметки в View

GO
func (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 честно обрежет хвост, который перестал влезать. С этим связано второе живое правило парсинга: пользователь вставит что угодно, включая пробелы и пустые строки, — и заметка на диске должна это пережить.

Сохранение заметки

GO
type 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 ещё раз, иначе текст будет упираться в невидимый резерв.

Главные методы и поля textarea#

Метод / полеЧто делает
Value / SetValueпрочитать весь текст / поставить целиком, с учётом лимита
InsertString / Resetвставить строку под курсором / очистить поле
Length / LineCountчисло символов / число строк значения
SetWidth / SetHeightразмеры поля; SetWidth — после Prompt и номеров строк
CharLimitпотолок символов; 0 — без лимита (по умолчанию)
MaxHeightограничение высоты сверху, по умолчанию 99 строк
ShowLineNumbersномера строк слева, по умолчанию включены
Focus / Blurвключить фокус (возвращает команду) / снять его

Проверка: ширина и номера строк

Вы вызвали SetWidth(50), затем отключили ShowLineNumbers. Что нужно, чтобы ширина пересчиталась?

Соберите Init и сохранение

Восстановите команду мигания, чтение значения и пересылку сообщений.

func (m noteModel) Init() tea.Cmd { return textarea. } 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.()) } } var cmd tea.Cmd m.ta, cmd = m.ta.(msg) return m, cmd }

Соберите конфигурацию поля

Расставьте строки: создание, содержимое-конфигурация, размеры, фокус.

Строки

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

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

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

Термин

CharLimit

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

Потолок символов на всё значение; по умолчанию 0 — лимита нет. Действует на ввод, вставку и SetValue.

1 / 5

Что дальше#

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

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

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

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

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

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

виртуальный терминал · bubbleteaalt-screen
ЗАМЕТКА — textarea с CharLimit 500
1 │ — купить хлеб
2 │ — написать отчёт
LineCount: 2 · Length считает символы по всему тексту
наберите строку в поле ниже и нажмите enter — она встанет в заметку

Клавиатура — обработано в 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, не запуская сохранение.

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

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

Поле не растёт по мере набора текста. Это так задумано?

Да: высота textarea фиксирована, а лишний текст прокручивается внутри, как в viewport. Если нужно ощущение роста, пересчитывайте SetHeight по LineCount в Update, не забывая про потолок MaxHeight. В полноэкранных приложениях высоту обычно привязывают к WindowSizeMsg, о котором будет урок про alt-screen.

Как ограничить длину отдельной строки?

Прямого ограничения на строку у компонента нет — CharLimit общий. Ставьте валидацию на своей стороне: в ветке сообщения или перед сохранением пройдитесь по Value, разбейте на строки и посчитайте длину каждой. Для заметок чаще всего хватает общего лимита и TrimSpace при разборе.

Вставка из буфера не работает в SSH-сессии. Почему?

Помимо обычной вставки терминалом, у textarea есть собственный запрос буфера обмена по ctrl+v через протокол OSC52, и его поддержка зависит от терминала. В большинстве случаев достаточно обычной вставки комбинацией терминала: текст придёт пачкой символов и попадёт в поле целиком, с учётом CharLimit.

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

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

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

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

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

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

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

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

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