329 lines
28 KiB
Markdown
329 lines
28 KiB
Markdown
# Мастер настроек GoSetup терминальная версия
|
||
|
||
Перенос [GoSetup+Fyne](https://dev.gossys.dev/XeRo/GoSetup) в терминал с использованием библиотек сообщества [charm](https://charm.sh/).
|
||
|
||
|
||
## Структура файла yaml
|
||
В файле yaml есть следующие корневые поля:
|
||
- name - имя приложения для настройки
|
||
- version - версия приложения
|
||
- description - краткое описание приложения
|
||
- type - тип приложения
|
||
- variables - массив переменных, в которых будет храниться нужная информация
|
||
- sections - массив секций, форм для разделения этапов настройки
|
||
|
||
### Переменные
|
||
Переменные хранят значение для настройки, в них записываются значения полей, используются в условиях и для выполнений действий
|
||
Переменная имеет 3 поля:
|
||
- name - имя переменной, по имени идет связь со всем. Имя должно быть уникальным
|
||
- type - тип значения
|
||
- default - значение по умолчанию, если boolean, то указывайте без кавычек
|
||
|
||
### Секции
|
||
Каждая секция это отдельна форма.
|
||
Секция имеет следующие поля:
|
||
- name - имя секции
|
||
- type - тип секции, по умолчанию тип не указывается, в таком случае секция содержит поля ввода. Если type About, то мы выводим подробное описание приложения. Если type Final, то выводим таблицу переменных со всеми значениями. About ставьте первой секцией, а Final последней.
|
||
- description - описание секции, что в ней прописывается. Пишите коротко
|
||
- condition - условие при котором эта секция отображается
|
||
- parent - массив имен переменных от которых зависит условие
|
||
- paragraphs - массив строк, используется если type About. Используются следующие правила:
|
||
- '#' Заголовок первого уровня
|
||
- '##' Заголовок второго уровня
|
||
- '###' Заголовок третье уровня
|
||
- '-' элемент списка
|
||
- простой текст
|
||
- fields - массив полей для вывода
|
||
- actions - массив действий, которые должны выполнится после перехода с этой секции
|
||
|
||
#### Поле
|
||
Это свойство описывает поле для вывода на форму. Имеет следующие свойства:
|
||
- name - имя поля, выводится слева от самого поля
|
||
- variable-name - переменная которой будет присваиваться значение из пол
|
||
- field-type - тип поля, предусмотрены следующие типы
|
||
- select - поле выпадающее списка, среди которых выбирается вариант из списка. Для указания значений заполняется также поле "options" куда вводится массив значений
|
||
- check - поле чекбокс, для булевых значений.
|
||
- file - поле выбора файла. Это двух составное поле, в обычном состоянии в него вводится текст, но если нажать enter, то открывается список, где перечислены файлы и директории которые соответствуют пути. Также используется поле file-field-extensions, представляющее массив строк с расширением файлов
|
||
- directory - поле выбора директории. Это двух составное поле, как и 'file', но предназначен только для работы с папками. У него нет дополнительного поля.
|
||
- compute - поле для вывода расчетных значений. Когда значение требуется получить путем не ввода или выбора, а через функции, например регулярные выражения.
|
||
- default - поле для простого ввода текста. Его не нужно прописывать отдельно, если field-type не равен значениям выше, то будет применяться простое поле ввода.
|
||
- description - описание поля. Выводится под полем ввода.
|
||
- condition - условие при котором это поле отображается
|
||
- options - массив объектов для поля типа select
|
||
- special-value - когда значение поля должны быть рассчитаны
|
||
- compute - строка по которой надо произвести вычисление
|
||
- validate - массив правил валидации поля, если поле не соответствует правилу, то вместо описания поля отображается сообщение ошибки валидации
|
||
- parent - массив имен переменных от которых зависит условие
|
||
|
||
##### Валидация
|
||
Поля поддержаны валидации через специальное поле Validate содержащее массив правил валидации. Правило валидации содержит 2 свойства:
|
||
- rule - правила которому нужно проверять значение
|
||
- message - сообщение, которое выводится при нарушении правила.
|
||
|
||
|
||
#### Действие
|
||
Действие это то, что необходимо выполнить после завершение ввода текущей секции. После нажатия клавиши следующая секция, запускается поток выполняющий все действия по порядку их указания. Во время выполнения открывается модальное окно загрузки. Если все действия секции выполнены, то окно загрузки закрывается и появляется следующая секции с учетом условий секций. Если действие завершилось ошибкой, появляется окно предупреждение. После его закрытияя принудительно показывается окно логирования.
|
||
Действие имеет следующие свойства:
|
||
- name - имя выполняемого действия
|
||
- type - тип действия. Может быть command или file_template, в ином случае будет ошибка непредвиденного типа действия
|
||
- command - команда которая должна быть выполнена, помещаются простые команды оболочки bash. Используется если тип 'command'
|
||
- file-source - файл, который выступает основой. В нем по шаблону прописываются параметры куда надо подставить значения переменных
|
||
- file-target - файл, который должен создаться на основе файла источника с подставленными значениям
|
||
- require-packages - список необходимых для выполнения действия пакетов. Если пакет не установлен, то установка выполнится автоматически. Из-за возникновения блокировок apt когда внутри одного действия apt вызывается иное действие apt. Лучше прописывать require-packages внутри control файла.
|
||
- error - описание ошибки выполнения действия
|
||
- assign-to - имя переменной которой нужно присвоенное значение выполнения команды. В случаях, когда для выполнения действия требуется знать результат другого действия, можно воспользоваться присвоением значения в переменную и после использовать.
|
||
|
||
### Условия
|
||
Условия это динамическая возможность работать только с теми полями и секциями, которые нуждаются при текущих параметрах. Проверка условий выполняются благодаря библиотеке [expr](https://github.com/expr-lang/expr). Правила условий:
|
||
- обязательный возврат результата в формате boolean значения
|
||
- указывается имя переменной, а не имя поля
|
||
- имя переменной указывается напрямую, без дополнительных знаков, шаблонов и прочего
|
||
- имя переменной должно совпадать по регистру
|
||
- для указания дополнительных условий, используются логические операторы "and" или "&&" для операции "И" и операторы "or" и "||" для операции "ИЛИ"
|
||
- при сравнении переменной со строкой, требуется указывать кавычки
|
||
- нельзя сравнивать разные типы данных, например строку с числом
|
||
|
||
Для корректной работы требуется указывать "родителей" условия, переменные которые участвуют в нем. Это позволяет при проверке изменений данных указывать, что необходимо обновить форму, ведь от значения изменяемого поля зависит другие поля или секции.
|
||
|
||
### Шаблоны
|
||
Шаблоны это правила подстановки в строки значений переменных, благодаря этому получается сформировать строку с динамическими данными или целый файл. Для этого используется библиотека [html/template](https://pkg.go.dev/html/template). Правила шаблонов:
|
||
- переменная которая должна быть подставлена указывается следующим образом '{{.variable_name}}'
|
||
- имя переменной должно совпадать по регистру
|
||
|
||
В шаблонах также может быть добавлены условия. Выглядит это следующим образом
|
||
```text
|
||
used_protocol={{if .is_secure}}https:\\{{else}}http:\\{{end}}
|
||
```
|
||
В этом примере написано следующее, значение used_protocol равно https:// если переменная is_secure равна true иначе равно http://.
|
||
|
||
## Окна
|
||
В приложении прописаны несколько типов окон, также приложение максимально адаптировано под малый размер окна терминала. Минимальный размер окна терминала рассчитан на 60*20, при значениях меньше контент обрезается. Добавлен функционал переноса строк и прокручивание контента.
|
||
### Окно листа
|
||
Лист это первое окно, которое появляется при запуске. Лист нужен для выбора, какое приложение нужно будет настроить. Окно делится на 2 части, список приложений слева и короткое описание приложения справа. Переключаться между пунктами можно на стрелки вниз и вверх. При переключении автоматически меняется описание в блоке справа.
|
||

|
||
Если приложений в списке слишком много, то они автоматически переносятся на следующую страницу, с отображением текущей страницы. Для быстрого переключения между страницами можно использовать стрелки влево и вправо.
|
||

|
||

|
||

|
||
Для выбора приложения, используется клавиша enter, при её нажатии. Появляется [модальное окно](Readme.md#модальное-окно-подтверждения-выбора-настраиваемого-приложения) подтверждения выбора настраиваемого приложения, где нужно подтвердить выбор приложения для настройки
|
||
|
||
### Окно секции
|
||
Секция это блок посвященный конкретному разделу настройки приложений. В секции может находиться форма с полями, более подробное описание приложения или таблица переменных и их значений.
|
||
У секции могут быть условия отображения, что позволяет пропустить или задействовать определенные этапы настройки в зависимости от значений переменных или данных в системе.
|
||
Секция About предназначена для вывода подробной информации о проекте. По задумке эта секция должна идти первой, но если прописать её в других местах или несколько раз, система не выдаст ошибку. Если Разработчик сочтет нужным, он может добавлять About перед каждой секцией формы, для подробного объяснения каждого поля.
|
||

|
||
При переходе на следующую секцию, если у текущей секции прописаны действия запускается поток. В нем последовательно обрабатывается каждое действие, чье условие выполняется. Во время обработки появляется [модальное окно](Readme.md#модальное-окно-ожидания-выполнения-действий) ожидания выполненения действий.
|
||
Когда обработка закончится, открывается либо следующая секция, либо если действие завершилось с ошибкой, показывается [модальное окно](Readme.md#модальное-окно-предупреждения-об-ошибке) предупреждения об ошибке, а после окно логирования действий.
|
||
|
||
Секция Формы предназначена для предоставления различных полей для значений настройки приложения. Это могут быть простые поля ввода данных, раскрывающийся список для выбора из определенных опций, чекбокс для подтверждения или список выбора файла или директории. Каждое поле также может использовать условия для отображения.
|
||

|
||

|
||
Каждое поле также поддерживает валидацию. Что позволяет заранее узнать о возможных ошибках при настройке. Важно, в текущей версии GoSetup ошибка валидации не запрещает использовать введенное значение.
|
||

|
||
|
||
Секция Final предназначена для вывода вводимых данных после настройки. В этот момент пользователь может проверить все ли вводимые им данные верны.
|
||

|
||

|
||
После нажатия клавиши enter запускаются последние действия, если они выполнились успешно, то появится [модальное окно](Readme.md#модальное-окно-завершения-настройки) завершения настройки. При его нажатии настройка завершается и приложения закрывается, возвращая статус код 0.
|
||
|
||
### Модальные окна
|
||
Модальны окна предназначены для вывода сообщений на экран и получения подтверждений действий пользователя. Есть следующие модальные окна.
|
||
#### Модальное окно подтверждения выбора настраиваемого приложения
|
||

|
||
#### Модальное окно подтверждения возврата к списку приложений
|
||

|
||
#### Модальное окно подтверждения выхода
|
||

|
||
#### Модальное окно завершения настройки
|
||

|
||
#### Модальное окно ожидания выполнения действий
|
||

|
||
#### Модальное окно предупреждения об ошибке
|
||

|
||
|
||
### Окно логирования
|
||
Для просмотра результатов выполняемых действий секции, предусмотрено окно Логов, куда по мере выполнения действия добавляются сообщения результаты. Окно открывается автоматически если во время выполнения произошла ошибка, также есть горячая клавиша ["ctrl+l"](Readme.md#горячие-клавиши), что скрывает или отображает окно.
|
||

|
||

|
||

|
||

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

|
||
|
||
## Горячие клавиши
|
||
Для работы с приложением заранее определен ряд клавиш:
|
||
- Клавиши движения. Эти клавиши предназначены для смены фокуса между элементами. К ним относятся
|
||
- up, shift+tab - перемещают фокус выше, на прошлый элемент
|
||
- down, tab - перемещают фокус ниже, на следующий элемент
|
||
- left - перемещают фокус левее
|
||
- right - перемещают фокус правее
|
||
- home - прокрутка к старту области
|
||
- end - прокрутка к окончанию области
|
||
- Клавиши взаимодействия. Эти клавиши отвечают за работу непосредственно с процессом настройки. К ним относятся
|
||
- enter - подтверждает действие, например открытие поля, перехода на следующую секцию или модального окна
|
||
- ctrl+l - открывает или закрывает окно логирования
|
||
- esc - закрывает открытое поля списка, если фокус не был на открытом поле списка, то появляется [модальное окно](Readme.md#модальное-окно-подтверждения-возврата-к-списку-приложений) подтверждения возврата к списку приложений
|
||
- ctrl+c, ctrl+q - открывает [модальное окно](Readme.md#модальное-окно-подтверждения-выхода) подтверждения выхода
|
||
|
||
## Дополнительно, Важное
|
||
В текущей итерации не учтен ряд элементов. Это могут быть вопросы реализации, правил поведения приложения, удобство пользования.
|
||
|
||
## Собственные функции для expr
|
||
Библиотека [expr](https://github.com/expr-lang/expr) имеет ряд готовых функций их можно просмотреть [здесь](https://github.com/expr-lang/expr/blob/master/builtin/builtin.go). Также для были определенны собственные функции, которые можно посмотреть в файле [здесь](setup/utils/utils.go#L153)
|
||
```go
|
||
// DefineExprCustomFunctions возвращает собственные функции expr
|
||
func DefineExprCustomFunctions() []expr.Option {
|
||
return []expr.Option{
|
||
//вычисляет значение по заданному регулярному выражению
|
||
expr.Function("regex",
|
||
func(args ...interface{}) (interface{}, error) {
|
||
if len(args) != 3 {
|
||
return "", fmt.Errorf("regex() ожидает 3 аргумента")
|
||
}
|
||
input := fmt.Sprintf("%v", args[0])
|
||
pattern := fmt.Sprintf("%v", args[1])
|
||
repl := fmt.Sprintf("%v", args[2])
|
||
re, err := regexp.Compile(pattern)
|
||
if err != nil {
|
||
return input, nil
|
||
}
|
||
return re.ReplaceAllString(input, repl), nil
|
||
}, new(func(string, string, string) string),
|
||
),
|
||
//проверяет совпадение переданного значения и указанного регулярного выражения
|
||
expr.Function("regex_match",
|
||
func(args ...interface{}) (interface{}, error) {
|
||
if len(args) != 2 {
|
||
return false, fmt.Errorf("regex_match() ожидает 2 аргумента")
|
||
}
|
||
input := fmt.Sprintf("%v", args[0])
|
||
pattern := fmt.Sprintf("%v", args[1])
|
||
|
||
re, err := regexp.Compile(pattern)
|
||
if err != nil {
|
||
return false, fmt.Errorf("некорректный regex %s, err:%v", pattern, err)
|
||
}
|
||
return re.MatchString(input), nil
|
||
|
||
}, new(func(string, string) bool),
|
||
),
|
||
//возвращает собственный текст ошибки
|
||
expr.Function("error",
|
||
func(args ...interface{}) (interface{}, error) {
|
||
if len(args) == 0 {
|
||
return nil, fmt.Errorf("error() требует сообщения")
|
||
}
|
||
return nil, fmt.Errorf("%v", args[0])
|
||
},
|
||
new(func(string) error),
|
||
),
|
||
//возвращает тело ответа по указанному адресу
|
||
expr.Function("http_get",
|
||
func(args ...interface{}) (interface{}, error) {
|
||
if len(args) == 0 {
|
||
return "", nil
|
||
}
|
||
url := fmt.Sprintf("%v", args[0])
|
||
resp, err := http.Get(url)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
defer resp.Body.Close()
|
||
body, _ := io.ReadAll(resp.Body)
|
||
return string(body), nil
|
||
},
|
||
new(func(string) string),
|
||
),
|
||
//возвращает результат выполнения команды из оболочки
|
||
expr.Function("shell",
|
||
func(args ...interface{}) (interface{}, error) {
|
||
if len(args) == 0 {
|
||
return "", nil
|
||
}
|
||
cmd := fmt.Sprintf("%v", args[0])
|
||
parts := strings.Fields(cmd)
|
||
if len(parts) == 0 {
|
||
return "", nil
|
||
}
|
||
out, err := exec.Command(parts[0], parts[1:]...).CombinedOutput()
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
return strings.TrimSpace(string(out)), nil
|
||
},
|
||
new(func(string) error),
|
||
),
|
||
//возвращает значение указанной переменной из env
|
||
expr.Function("env",
|
||
func(args ...interface{}) (interface{}, error) {
|
||
if len(args) == 0 {
|
||
return "", nil
|
||
}
|
||
return os.Getenv(fmt.Sprintf("%v", args[0])), nil
|
||
}, new(func(string) error),
|
||
),
|
||
//проверяет наличие файла в системе
|
||
expr.Function("file_exists",
|
||
func(args ...interface{}) (interface{}, error) {
|
||
if len(args) == 0 {
|
||
return false, nil
|
||
}
|
||
path := fmt.Sprintf("%v", args[0])
|
||
fileInfo, err := os.Stat(path)
|
||
if err == nil && !fileInfo.IsDir() {
|
||
return true, nil
|
||
} else if err == nil && fileInfo.IsDir() {
|
||
return false, fmt.Errorf("file is dir")
|
||
}
|
||
if errors.Is(err, fs.ErrNotExist) {
|
||
return false, nil
|
||
} else {
|
||
return false, err
|
||
}
|
||
}, new(func(string) bool),
|
||
),
|
||
//проверяет наличие директории в системе
|
||
expr.Function("dir_exists",
|
||
func(args ...interface{}) (interface{}, error) {
|
||
if len(args) == 0 {
|
||
return false, nil
|
||
}
|
||
path := fmt.Sprintf("%v", args[0])
|
||
fileInfo, err := os.Stat(path)
|
||
if err == nil && fileInfo.IsDir() {
|
||
return true, nil
|
||
} else if err == nil && !fileInfo.IsDir() {
|
||
return false, fmt.Errorf("file is not a dir")
|
||
}
|
||
if errors.Is(err, fs.ErrNotExist) {
|
||
return false, nil
|
||
} else {
|
||
return false, err
|
||
}
|
||
}, new(func(string) bool),
|
||
),
|
||
//проверяет доступность порта
|
||
expr.Function("port_free",
|
||
func(args ...interface{}) (interface{}, error) {
|
||
if len(args) == 0 {
|
||
return false, nil
|
||
}
|
||
port := fmt.Sprintf("%v", args[0])
|
||
cmd := exec.Command("ss", "-ltn", "sport =: "+port)
|
||
return cmd.Run() != nil, nil
|
||
}, new(func(string) error),
|
||
),
|
||
//проверяет наличие пользователя в системе
|
||
expr.Function("user_exists",
|
||
func(args ...interface{}) (interface{}, error) {
|
||
if len(args) == 0 {
|
||
return false, nil
|
||
}
|
||
name := fmt.Sprintf("%v", args[0])
|
||
cmd := exec.Command("id", "-u", name)
|
||
return cmd.Run() == nil, nil
|
||
}, new(func(string) bool),
|
||
),
|
||
}
|
||
}
|
||
```
|