DevGang
Авторизоваться

Обработка пользовательского ввода в Bubble Tea с помощью компонента меню

Создавайте привлекательные приложения командной строки с помощью Bubble Tea (серия из 2 частей):

  1. Вступление к Bubble Tea в Go
  2. Обработка пользовательского ввода в 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 частей):

  1. Вступление к Bubble Tea в Go
  2. Обработка пользовательского ввода в Bubble Tea с помощью компонента меню
#Golang
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу