| bootstrap | ||
| cmd | ||
| confs | ||
| public | ||
| setup | ||
| types | ||
| .gitignore | ||
| go.mod | ||
| go.sum | ||
| Makefile | ||
| Readme.md | ||
Мастер настроек GoSetup терминальная версия
Перенос GoSetup+Fyne в терминал с использованием библиотек сообщества charm.
Структура файла 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. Правила условий:
- обязательный возврат результата в формате boolean значения
- указывается имя переменной, а не имя поля
- имя переменной указывается напрямую, без дополнительных знаков, шаблонов и прочего
- имя переменной должно совпадать по регистру
- для указания дополнительных условий, используются логические операторы "and" или "&&" для операции "И" и операторы "or" и "||" для операции "ИЛИ"
- при сравнении переменной со строкой, требуется указывать кавычки
- нельзя сравнивать разные типы данных, например строку с числом
Для корректной работы требуется указывать "родителей" условия, переменные которые участвуют в нем. Это позволяет при проверке изменений данных указывать, что необходимо обновить форму, ведь от значения изменяемого поля зависит другие поля или секции.
Шаблоны
Шаблоны это правила подстановки в строки значений переменных, благодаря этому получается сформировать строку с динамическими данными или целый файл. Для этого используется библиотека html/template. Правила шаблонов:
- переменная которая должна быть подставлена указывается следующим образом '{{.variable_name}}'
- имя переменной должно совпадать по регистру
В шаблонах также может быть добавлены условия. Выглядит это следующим образом
used_protocol={{if .is_secure}}https:\\{{else}}http:\\{{end}}
В этом примере написано следующее, значение used_protocol равно https:// если переменная is_secure равна true иначе равно http://.
Окна
В приложении прописаны несколько типов окон, также приложение максимально адаптировано под малый размер окна терминала. Минимальный размер окна терминала рассчитан на 60*20, при значениях меньше контент обрезается. Добавлен функционал переноса строк и прокручивание контента.
Окно листа
Лист это первое окно, которое появляется при запуске. Лист нужен для выбора, какое приложение нужно будет настроить. Окно делится на 2 части, список приложений слева и короткое описание приложения справа. Переключаться между пунктами можно на стрелки вниз и вверх. При переключении автоматически меняется описание в блоке справа.

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



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

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

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

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


После нажатия клавиши enter запускаются последние действия, если они выполнились успешно, то появится модальное окно завершения настройки. При его нажатии настройка завершается и приложения закрывается, возвращая статус код 0.
Модальные окна
Модальны окна предназначены для вывода сообщений на экран и получения подтверждений действий пользователя. Есть следующие модальные окна.
Модальное окно подтверждения выбора настраиваемого приложения
Модальное окно подтверждения возврата к списку приложений
Модальное окно подтверждения выхода
Модальное окно завершения настройки
Модальное окно ожидания выполнения действий
Модальное окно предупреждения об ошибке
Окно логирования
Для просмотра результатов выполняемых действий секции, предусмотрено окно Логов, куда по мере выполнения действия добавляются сообщения результаты. Окно открывается автоматически если во время выполнения произошла ошибка, также есть горячая клавиша "ctrl+l", что скрывает или отображает окно.




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

Горячие клавиши
Для работы с приложением заранее определен ряд клавиш:
- Клавиши движения. Эти клавиши предназначены для смены фокуса между элементами. К ним относятся
- up, shift+tab - перемещают фокус выше, на прошлый элемент
- down, tab - перемещают фокус ниже, на следующий элемент
- left - перемещают фокус левее
- right - перемещают фокус правее
- home - прокрутка к старту области
- end - прокрутка к окончанию области
- Клавиши взаимодействия. Эти клавиши отвечают за работу непосредственно с процессом настройки. К ним относятся
- enter - подтверждает действие, например открытие поля, перехода на следующую секцию или модального окна
- ctrl+l - открывает или закрывает окно логирования
- esc - закрывает открытое поля списка, если фокус не был на открытом поле списка, то появляется модальное окно подтверждения возврата к списку приложений
- ctrl+c, ctrl+q - открывает модальное окно подтверждения выхода
Дополнительно, Важное
В текущей итерации не учтен ряд элементов. Это могут быть вопросы реализации, правил поведения приложения, удобство пользования.
Установка пакетов
Из-за вызова команд apt при уже выполняем вызове, возникает блокировка. По этой причине, если использовать GoSetup для настройки устанавливаемого apt пакета и будет вызвана функция установки необходимого пакета, то это приведет к блокировке базы APT. Следовательно, нужно определять вызов программы и ограничивать часть действий. Например, запускать программу с аргументом, значащим вызов во время установки, по его наличию запрещать установку пакета и выводить сообщение или же при его отсутствии устанавливать нужный пакет.
Валидация
Сейчас валидация нужна для показания, что вводимое значение не отвечает правилами. Если пользователь намеренно или нет, проигнорирует ошибку поля, это может привести к ошибке настройки. Например, если введет имя пользователя русскими буквами или неподходящими символами, то при попытке выполнить команду, это приведет к ошибке действия. Требуется либо сделать запретить переход на следующую секцию при наличии ошибок валидации используемых полей, либо сделать это опциональным, в зависимости от правил.
Смена секций
Изначально было 2 варианта вызова действий. Первый был основан на запуске всех действия после последней секции, в таком варианте, был допустим переход на прошлые секции и смена их значений. На последней секции можно было просмотреть все введенные данные, а после подтверждения запускались бы все действия. Второй вариант основна на запуске действия по секциям. После нажатия кнопки перехода запускались действия конкретной секции. Было решено остановится на втором варианте для упрощения настройки. При возникновении ошибки действий первых секций, настройка останавливается, если подобное произойдет в первом варианте, то весь процесс ввода данных требовалось бы повторять вновь.
Из-за этого не получись сделать функционал возврата к прошлым секциям, ведь это могло вызвать путаницу действий. Если от значений в секции 1 зависит секция 2 и 3 с разными, противоречащими друг другу действиями, то возврат на секцию 1 после действия секции 2 и изменении данных для секции 3, вызовет конфликт системы из-за действия секции 2.
Внешний вид
Пока что не был определен единый стиль для программы. Цвета, границы, фон и прочее должен быть изменен для формирования единого стиля приложения.
Секция About
Из-за ограничений терминала нельзя сделать разный размер текста. То есть идентификация какого уровня заголовок, первого или второго, с помощью разного размера невозможна. Это привод к ограничению стилей markdown для описания.
Единый формат не стандартных значений
В данный момент есть обработка special-value и обработка compute. Они используются для получения значений, которые не предусмотрены для ввода напрямую или же сложны в расчете самостоятельно. Для special-value сейчас есть одна обработка code:users, её результат это массив строк пользователей системы, чей uid больше или равны 1000. Эта обработка сделана для установки как значения уже сущетсвующего пользователя. Подобная обработка одна на всю систему, для целесообразности special-value нужны другие варианты использования, иначе придется переформировать её расчет через compute.
Собственные функции для expr
Библиотека expr имеет ряд готовых функций их можно просмотреть здесь. Также для были определенны собственные функции, которые можно посмотреть в файле здесь
// 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),
),
}
}





