Что может быть интереснее, чем создание реальных приложений? 🤔
для меня — это научить людей создавать их снова!
Наше исследование
Как я уже рассказывал о своем проекте 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