Обработка пользовательского ввода в Bubble Tea с помощью компонента меню
Создавайте привлекательные приложения командной строки с помощью Bubble Tea (серия из 2 частей):
- Вступление к Bubble Tea в Go
- Обработка пользовательского ввода в Bubble Tea с помощью компонента меню
В предыдущей статье мы создали приложение «hello world», и оно обработало всего лишь немного пользовательского ввода («нажмите Ctrl + C для выхода»).
Но мы действительно не поняли, как на самом деле использовать пользовательский ввод для изменения данных модели и, в свою очередь, изменить то, что мы видим в приложении. Итак, в этом уроке мы создадим компонент меню, который позволит нам перемещаться между кнопками.
📝 Определение наших данных
Первое, что нам нужно для любого компонента Bubble Tea - это данные, за которые отвечает наша модель. Если вы помните в нашей модели simplePage данными был просто текст, который мы отображали:
type simplePage struct { text string }
В нашем меню нам нужно сделать следующее:
- Показать наши варианты
- Показать какой вариант выбран
- Кроме того, позвольте пользователю нажать на клавишу Enter, чтобы перейти на другую страницу. Но мы рассмотрим это чуть позже.
- На данный момент мы все еще можем передать функцию onPress, которая сообщает нам, что мы делаем, если пользователь нажимает Enter.
Итак, данные нашей модели будут выглядеть следующим образом: если вы следуете этому примеру, запишите это в файл с именем menu.go
.
type menu struct {
options []menuItem
selectedIndex int
}
type menuItem struct {
text string
onPress func() tea.Msg
}
Меню состоит из пунктов, и каждый пункт содержит текст и функцию, обрабатывающую нажатие клавиши Enter. В этом уроке мы просто заставим приложение переключаться между прописными и строчными буквами, чтобы оно хотя бы что-то делало.
Он возвращает tea.Msg
, потому что мы можем изменять данные в ответ на этот пользовательский ввод. Мы увидим, почему в следующем разделе, когда будем реализовывать интерфейс Model
.
Реализация интерфейса Model
Если вы помните, чтобы мы могли использовать нашу модель в качестве UI компонента, она должна реализовать этот интерфейс:
type Model interface {
Init() Cmd
Update(msg Msg) (Model, Cmd)
View() string
}
Сначала давайте напишем функцию Init.
func (m menu) Init() tea.Cmd { return nil }
Опять же, у нас нет никакого начального Cmd
, который нам нужно запустить, поэтому мы можем просто вернуть nil
.
Для функции View
давайте создадим меню старой школы со стрелкой, указывающей нам, какой элемент выбран в данный момент.
func (m menu) View() string {
var options []string
for i, o := range m.options {
if i == m.selectedIndex {
options = append(options, fmt.Sprintf("-> %s", o.text))
} else {
options = append(options, fmt.Sprintf(" %s", o.text))
}
}
return fmt.Sprintf(`%s
Press enter/return to select a list item, arrow keys to move, or Ctrl+C to exit.`,
strings.Join(options, "\n"))
}
Как упоминалось в предыдущей статье, одна из вещей, которая делает Bubble Tea действительно обучаемым, заключается в том, что отображение вашего UI - это, по сути, одна большая строка. Так и в menu.View
мы создаем фрагмент строк, в котором выбранный параметр имеет стрелку, а не выбранные параметры имеют начальные пробелы. Затем мы соединяем их все вместе и добавляем наши элементы управления внизу.
Наконец-то, давайте напишем наш метод Update для обработки пользовательского ввода.
func (m menu) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg.(type) {
case tea.KeyMsg:
switch msg.(tea.KeyMsg).String() {
case "ctrl+c":
return m, tea.Quit
case "down", "right", "up", "left":
return m.moveCursor(msg.(tea.KeyMsg)), nil
}
}
return m, nil
}
func (m menu) moveCursor(msg tea.KeyMsg) menu {
switch msg.String() {
case "up", "left":
m.selectedIndex--
case "down", "right":
m.selectedIndex++
default:
// do nothing
}
optCount := len(m.options)
m.selectedIndex = (m.selectedIndex + optCount) % optCount
return m
}
Метод Update - самая сложная часть этого приложения, поэтому давайте разберем его.
case "ctrl+c":
return m, tea.Quit
Как и раньше, мы обрабатываем тип KeyMsg
, и мы нажимаем Ctrl+C, чтобы выйти из приложения, вернув Quit cmd.
case "down", "right", "up", "left":
return m.moveCursor(msg.(tea.KeyMsg)), nil
Однако для клавиш со стрелками мы используем вспомогательную функцию moveCursor
, которая возвращает обновленную модель.
func (m menu) moveCursor(msg tea.KeyMsg) menu {
switch msg.String() {
case "up", "left":
m.selectedIndex--
case "down", "right":
m.selectedIndex++
default:
// do nothing
}
optCount := len(m.options)
m.selectedIndex = (m.selectedIndex + optCount) % optCount
return m
}
Строки «вверх» и «влево» KeyMsg служат для навигации вверх, а «вниз» и «вправо» перемещают нас вниз, уменьшая и увеличивая m.selected
.
Затем мы используем оператор mod, чтобы убедиться, что m.selected
является одним из индексов наших опций.
Наконец, при обновлении модели moveCursor
возвращает модель, которая возвращается функцией Update
, и новая модель в конечном итоге обрабатывается нашим методом View
.
Прежде, чем мы перейдем к обработке клавиши Enter, мы должны увидеть запуск нашего приложения. Давайте поместим наш новый компонент меню в основную функцию и запустим его.
func main() {
m := menu{
options: []menuItem{
menuItem{
text: "new check-in",
onPress: func() tea.Msg { return struct{}{} },
},
menuItem{
text: "view check-ins",
onPress: func() tea.Msg { return struct{}{} },
},
},
}
p := tea.NewProgram(m)
if err := p.Start(); err != nil {
panic(err)
}
}
На данный момент onPress - это просто функция без операции, которая возвращает пустую структуру. Теперь давайте запустим наше приложение.
go build
./check-ins
Вы должны увидеть что-то вроде этого:
Теперь в меню можно переключать то, что выбрано. Теперь давайте разберемся с этим пользовательским вводом.
✅ Обработка клавиши Enter и просмотр того, что на самом деле делает тип tea.Cmd
До сих пор мы по настоящему внимательно не рассматривали тип tea.Cmd
. Это одно из двух возвращаемых значений для метода Update
, но до сих пор мы использовали его только для выхода из приложения. Давайте подробнее рассмотрим его сигнатуру типа.
type Cmd func() tea.Msg
Cmd
- это своего рода функция, которая выполняет некоторые действия, а затем возвращает нам tea.Msg
. Эта функция может быть временной, это может быть ввод-вывод, например, извлечение каких-то данных, на самом деле все идет своим чередом. Tea.Msg
используется нашей функцией Update
для обновления нашей модели и, наконец, нашего представления.
Таким образом, обработка нажатия пользователем клавиши Enter и последующего запуска произвольной функции onPress является одним из таких способов использования Cmd. Итак, давайте начнем с обработчика кнопки Enter.
case tea.KeyMsg:
switch msg.(tea.KeyMsg).String() {
case "q":
return m, tea.Quit
case "down", "right", "up", "left":
return m.moveCursor(msg.(tea.KeyMsg)), nil
+ case "enter", "return":
+ return m, m.options[m.selectedIndex].onPress
}
Обратите внимание, что когда пользователь нажимает клавишу Enter, мы возвращаем модель без изменений, но мы также возвращаем функцию onPress
выбранного элемента. Если вы помните, когда мы определяли тип menuItem
, тип его поля onPress
был func() tea.Msg
. Другими словами, это точно соответствует псевдониму типа Cmd
.
Однако есть еще одна вещь, которую нам нужно сделать внутри метода Update
. Прямо сейчас мы обрабатываем только тип tea.KeyMsg
. Тип, который мы возвращаем для переключения заглавных букв выбранного элемента, будет совершенно новым типом tea.Msg
, поэтому нам нужно определить его, а затем добавить в него регистр в наш метод Update. Во-перых, давайте определим структуру.
type toggleCasingMsg struct{}
Нам не нужно передавать никаких данных, поэтому наш Msg - это просто пустая структура; если вы помните, тип tea.Msg
- это просто пустой интерфейс, поэтому мы просто можем сделать так, чтобы Msg содержал столько данных, сколько нам нужно.
Давайте вернемся к методу Update и добавим пример для toggleCasingMsg
.
Сначала добавьте метод toggleSelectedItemCase
func (m menu) toggleSelectedItemCase() tea.Model {
selectedText := m.options[m.selectedIndex].text
if selectedText == strings.ToUpper(selectedText) {
m.options[m.selectedIndex].text = strings.ToLower(selectedText)
} else {
m.options[m.selectedIndex].text = strings.ToUpper(selectedText)
}
return m
}
Затем добавьте его в метод Update
func (m menu) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg.(type) {
+ case toggleCasingMsg:
+ return m.toggleSelectedItemCase(), nil
case tea.KeyMsg:
// our KeyMsg handlers here
В toggleCasingMsg мы обновляем оболочку выбранного пункта меню, а затем возвращаем обновленную модель.
Наконец, в app.go воспользуемся нашим toggleCasingMsg
menuItem{
text: "new check-in",
- onPress: func() tea.Msg { return struct{}{} },
+ onPress: func() tea.Msg { return toggleCasingMsg{} },
},
menuItem{
text: "view check-ins",
- onPress: func() tea.Msg { return struct{}{} },
+ onPress: func() tea.Msg { return toggleCasingMsg{} },
},
Теперь давайте запустим наше приложение.
go build
./check-ins
Приложение должно выглядеть следующим образом:
Обратите внимание, что на данном этапе приложения это не единственный способ, которым мы могли бы обработать Enter; также могли бы полностью обработать все переключения в функции Update, вместо того, чтобы обрабатывать его с помощью Cmd.
Причина, по которой было использовано Cmd, заключается в следующем:
- Чтобы показать простой пример использования не завершающего действия Cmd в Bubble Tea
- Используя Cmd, мы можем передавать произвольные функции обработчика событий в четыре компонента, аналогичный шаблон, если вы закодировали в React.
Далее у нас есть меню, но пока оно не очень яркое.
Создавайте привлекательные приложения командной строки с помощью Bubble Tea (серия из 2 частей):