Как создать простую интерактивную оболочку в Go

Что может быть интереснее, чем создание реальных приложений? 🤔
для меня — это научить людей создавать их снова!

Наше исследование

Как я уже рассказывал о своем проекте Dockeroller ранее в своих статьях, это проект, который помогает вам управлять вашим docker deamon через различные гейты, такие как Telegram или API.

Я понял, что эти гейты должны легко настраиваться пользователем,
Мы должны иметь возможность включать или выключать некоторые гейты, менять их токены, порты, пароли и т.д.
Один из подходов — использование Viper и файла dockeroller.yml (реализация и обучение этому подходу находится в моем todo list).

Другим подходом может быть интерактивная оболочка!
Давайте сделаем это!

Готовый код доступен в моем Github gists (Ссылка).

Добро пожаловать на сцену!

Старайтесь быть чистыми, разделяя вещи!
В данном случае, я разделил свой код на различные этапы,
например:

  • этап приветствия
  • этап помощи
  • Этап ворот
  • Telegram
  • API

Каждый этап имеет сообщение, которое мы определили с помощью констант в начале кода, в больших проектах они могут быть помещены, например, в пакет msg.

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

const (
    msg_welcome string = `
Welcome to Dockeroller!
It's sample multiline!
`

    msg_help = `
Dockeroller is an open-source project made for fun.
It's sample multiline!
`
Вход в полноэкранный режим Выйти из полноэкранного режима

Делать это вечно!

Простейшая форма интерактивных оболочек, использующая бесконечный цикл, который повторяется до тех пор, пока вы его не прервете,
Но не волнуйтесь, это совсем не плохо, я расскажу вам почему.

На каждой итерации мы получаем что-то новое от пользователя, в это время наше приложение не потребляет никаких ресурсов,
После передачи желаемого ввода в оболочку, она обработает результат и перейдет к следующей итерации.
На самом деле, наша оболочка постоянно спит между итерациями, она ждет, пока мы передадим ей наши входные данные.

Взгляните на этот код:

func main() {
  var stage int = 0
  for {
    switch stage {
    case 0:
      stage = StageWelcome()
    case 1:
      stage = StageHelp()
    case 2:
      stage = StageGates()
    case 11:
      stage = StageTelegram()
    case 12:
      stage = StageAPI()
    }
  }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Функции, начинающиеся со слова StageXxx() — это наши обработчики, они обрабатывают то, что происходит на этапе (на 😅), мы скоро их рассмотрим.

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

Вкратце
У нас есть цикл for, мы выполняем итерации по нему и в каждой итерации проверяем, на какой стадии мы находимся (с помощью переключателя) и обрабатываем эту стадию.

Кроме того, мы обновляем наш этап после каждой итерации, но зачем?

Куда бы вы хотели перейти после?

Пришло время обновить наше приветственное сообщение!

const (
    msg_welcome string = `
Welcome to Dockeroller!
Where would you like to go after? (choose only number)
1 - help
2 - gates
`
Вход в полноэкранный режим Выход из полноэкранного режима

Итак, мы позволяем пользователю решать, какой этап будет его следующим местом!
И это причина, по которой мы обновляем сцену каждый раз.
Обновление переменной stage является частью навигации между этапами.
И каждый обработчик, в соответствии со своими входными данными, будет решать, куда нам идти дальше.

Ввод и вывод оболочки

В чем смысл этой статьи, если не заходить в сам терминал?
Это так просто, вы можете знать, как использовать функцию fmt.Print(), она печатает что-то на терминале, без какого-либо форматирования по умолчанию.
Есть и другие ее варианты, например fmt.Println() для печати с переводом строки и fmt.Printf() для печати с заданным форматом.

Хорошо, в пакете fmt есть еще несколько функций, здесь нам понадобится fmt.Scanln() или fmt.Scan() (есть также fmt.Scanf() для сканирования в соответствии с заданным форматом).

Давайте рассмотрим очень простой пример:

package main

import "fmt"

func main() {
  var name string
  fmt.Scanln(&name)
  fmt.Println("hello", name)
}
Вход в полноэкранный режим Выход из полноэкранного режима

Если вы не передадите указатель в Scanln, это не будет иметь никакого эффекта.

Мы можем добавить дополнительные операторы печати, чтобы сделать это более красивым.
Мы также можем передать ему несколько указателей разных типов, Scan разделит их пробелом и затем заполнит их.

package main

import "fmt"

func main() {
  var name string
  var age int
  fmt.Print("> ") // Just for beauty
  fmt.Scanln(&name, &age)
  fmt.Println("Hello", name, "you're", age, "years old.")
}
Вход в полноэкранный режим Выход из полноэкранного режима

Вывод будет выглядеть следующим образом:

Основные обработчики

Думаю, теперь все должно быть понятно:

// Welcome stage handler
func StageWelcome() int {
  fmt.Print(msg_welcome)
  return getStage()
}

// Help stage handler
func StageHelp() int {
  fmt.Print(msg_help)
  return getStage()
}

// Helper function for asking stage number and return it for next decisions
func getStage() (stage int) {
  fmt.Print("> ")
  fmt.Scanln(&stage)
  return
}
Вход в полноэкранный режим Выход из полноэкранного режима

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

Обработчики ворот

Добро пожаловать — это этап первого уровня
Help и Gates — второй уровень
Telegram & API — третий уровень
и может быть больше уровней.

Как мы можем это реализовать?
ну, есть много алгоритмов для этого, но здесь, если мы находимся на стадии ворот и должны перейти к варианту 1 (telegram) или 2 (api), я добавляю 10 к номеру стадии, и в главном переключателе, telegram будет 11, а api будет 12.
Если мы хотим вернуться с этого уровня, мы вернем 0, что означает Welcome (stage) handler.

func StageGates() int {
  fmt.Print(msg_gates)
  if stage := getStage(); stage != 0 {
    return stage + 10
  }
  return 0
}
Вход в полноэкранный режим Выход из полноэкранного режима

Обработчики Telegram & API

Я просто объясняю один из них, следуйте тому же подходу.

func StageTelegram() int {
  fmt.Print(msg_telegram)

  // Get a value (token) 
  var token string
  getInput("Token: ", &token)

  // Get another value (username) 
  var username string
  getInput("Username: @", &username)
  // Telegram usernames start with @,
  // Some users may include it, some may not,
  // So we helped users by including it.
  // And username variable won't have @ in it.

  fmt.Println("Username and token successfully sat!")
  return 0 // Return to the main menu after succefull config
}

// Helper function to print a message and get a value (by populating a pointer)
func getInput(msg string, value interface{}) {
  fmt.Print(msg)
  fmt.Scanln(value) // DO NOT include &, because it's a pointer when is passed to this function!
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Заключительная цитата

Надеюсь, вам понравилась эта статья, не стесняйтесь делиться со мной своими мыслями и критикой.
Для создания более красивых пользовательских интерфейсов вы можете использовать этот пакет с открытым исходным кодом: promptui

Оцените статью
devanswers.ru
Добавить комментарий