Изменение вызова действий, проверка наличия действий секции, отображение счетчика выполнения действий в модальном окне, работа над подключением привязки горячих клавиш
This commit is contained in:
parent
91b1eea1d4
commit
c69a7ae84b
14 changed files with 886 additions and 706 deletions
|
|
@ -94,10 +94,10 @@ sections:
|
||||||
- "### Бла бла бла бла. \n lorem ipsum прочее прочее прочее прочее"
|
- "### Бла бла бла бла. \n lorem ipsum прочее прочее прочее прочее"
|
||||||
- "## Бла бла бла бла. \n lorem ipsum прочее прочее прочее прочее"
|
- "## Бла бла бла бла. \n lorem ipsum прочее прочее прочее прочее"
|
||||||
actions:
|
actions:
|
||||||
- name: "Test"
|
- name: "Sleep 1"
|
||||||
type: "command"
|
type: "command"
|
||||||
#command: "touch /etc/repko/{{.api_domain}}.txt"
|
#command: "touch /etc/repko/{{.api_domain}}.txt"
|
||||||
command: "sleep 2"
|
command: "sleep 1"
|
||||||
error: "ошибка создания файла"
|
error: "ошибка создания файла"
|
||||||
- name: "user"
|
- name: "user"
|
||||||
description: "Настройка данных пользователя"
|
description: "Настройка данных пользователя"
|
||||||
|
|
|
||||||
|
|
@ -1,466 +0,0 @@
|
||||||
package setup
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bubble/setup/utils"
|
|
||||||
"bubble/types"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"github.com/charmbracelet/bubbles/textinput"
|
|
||||||
"github.com/expr-lang/expr"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"io"
|
|
||||||
"io/fs"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"reflect"
|
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// userRE - с начала строки, делит все по знаку двоеточия, берется первые 3 группы, пример user_name:x:1000:
|
|
||||||
var userRE = regexp.MustCompile(`^([^:]+):[^:]*:([0-9]+):`)
|
|
||||||
|
|
||||||
// ParamsForCondition получение словаря переменных с приведением типов для проверки условий
|
|
||||||
func ParamsForCondition(app *types.PackageApplication) map[string]interface{} {
|
|
||||||
params := make(map[string]interface{})
|
|
||||||
|
|
||||||
for _, variable := range app.Variables {
|
|
||||||
value := variable.Value
|
|
||||||
if value == nil {
|
|
||||||
value = variable.Default
|
|
||||||
}
|
|
||||||
|
|
||||||
// Приводим к нужному типу, как в твоём старом коде
|
|
||||||
switch variable.Type {
|
|
||||||
case "bool":
|
|
||||||
if s, ok := value.(string); ok {
|
|
||||||
value = strings.ToLower(s) == "true"
|
|
||||||
}
|
|
||||||
case "number", "int", "float":
|
|
||||||
if s, ok := value.(string); ok {
|
|
||||||
if n, err := strconv.ParseFloat(s, 64); err == nil {
|
|
||||||
value = n
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
params[variable.Name] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
// Также добавляем значения из полей секций (на случай, если переменная используется только в поле)
|
|
||||||
for _, section := range app.Sections {
|
|
||||||
for _, field := range section.Fields {
|
|
||||||
if field.Variable == nil || !field.Active {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
value := field.Variable.Value
|
|
||||||
if value == nil {
|
|
||||||
value = field.Variable.Default
|
|
||||||
}
|
|
||||||
|
|
||||||
switch field.Variable.Type {
|
|
||||||
case "bool":
|
|
||||||
if s, ok := value.(string); ok {
|
|
||||||
value = strings.ToLower(s) == "true"
|
|
||||||
}
|
|
||||||
case "number", "int", "float":
|
|
||||||
if s, ok := value.(string); ok {
|
|
||||||
if n, err := strconv.ParseFloat(s, 64); err == nil {
|
|
||||||
value = n
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
params[field.VariableName] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return params
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckCondition проверяет равно ли переданное условие
|
|
||||||
func CheckCondition(condition string, app *types.PackageApplication) bool {
|
|
||||||
if condition == "" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := expr.Eval(condition, ParamsForCondition(app))
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
logrus.Errorf("Ошибка в условии '%s': %v", condition, err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
boolResult, ok := result.(bool)
|
|
||||||
if !ok {
|
|
||||||
logrus.Warnf("Условие '%s' не вернуло bool, а %T: %v", condition, result, result)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return boolResult
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParamsForCompute получение словаря переменных без приведения типов, для вычисления
|
|
||||||
func ParamsForCompute(app *types.PackageApplication) map[string]interface{} {
|
|
||||||
params := make(map[string]interface{})
|
|
||||||
|
|
||||||
for i := range app.Variables {
|
|
||||||
v := &app.Variables[i]
|
|
||||||
value := v.Value
|
|
||||||
if value == nil {
|
|
||||||
value = v.Default
|
|
||||||
}
|
|
||||||
params[v.Name] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
// Добавляем значения из активных полей
|
|
||||||
for _, section := range app.Sections {
|
|
||||||
for _, field := range section.Fields {
|
|
||||||
if field.Variable == nil || !field.Active {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
value := field.Variable.Value
|
|
||||||
if value == nil {
|
|
||||||
value = field.Variable.Default
|
|
||||||
}
|
|
||||||
params[field.VariableName] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return params
|
|
||||||
}
|
|
||||||
|
|
||||||
// ComputeValue вычисляет значение для поля compute.
|
|
||||||
func ComputeValue(computeExpr string, app *types.PackageApplication) (interface{}, error) {
|
|
||||||
if computeExpr == "" {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
env := ParamsForCompute(app)
|
|
||||||
options := append([]expr.Option{
|
|
||||||
expr.Env(env),
|
|
||||||
expr.AsKind(reflect.String), // по умолчанию строка, если не указано иное
|
|
||||||
expr.AllowUndefinedVariables(),
|
|
||||||
}, DefineExprCustomFunctions()...)
|
|
||||||
|
|
||||||
program, err := expr.Compile(computeExpr, options...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("ошибка компиляции compute: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := expr.Run(program, env)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("ошибка выполнения compute: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recompute производит перерасчет всех полей
|
|
||||||
func Recompute(app *types.PackageApplication) {
|
|
||||||
for i := range app.Sections {
|
|
||||||
section := &app.Sections[i]
|
|
||||||
for j := range section.Fields {
|
|
||||||
field := §ion.Fields[j]
|
|
||||||
if field.FieldType == "compute" && field.Compute != "" {
|
|
||||||
value, err := ComputeValue(field.Compute, app)
|
|
||||||
if err != nil {
|
|
||||||
value = fmt.Sprintf("Ошибка вычисления: %v", err)
|
|
||||||
}
|
|
||||||
field.Variable.Value = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ValidateCheckField(fieldName, variableName string, validations []types.FieldValidate, app *types.PackageApplication) types.BoolValidateFunc {
|
|
||||||
if len(validations) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return func(checked bool) error {
|
|
||||||
env := ParamsForCompute(app)
|
|
||||||
env[variableName] = checked
|
|
||||||
options := append([]expr.Option{
|
|
||||||
expr.Env(env),
|
|
||||||
expr.AllowUndefinedVariables(),
|
|
||||||
}, DefineExprCustomFunctions()...)
|
|
||||||
for _, validate := range validations {
|
|
||||||
program, err := expr.Compile(validate.Rule, options...)
|
|
||||||
prefix := fmt.Sprintf("Валидация поля %s,", fieldName)
|
|
||||||
if err != nil {
|
|
||||||
msg := fmt.Errorf("ошибка компиляции правила: %s, err: %v", validate.Rule, err)
|
|
||||||
logrus.Errorf("%s %v", prefix, msg)
|
|
||||||
return msg
|
|
||||||
}
|
|
||||||
result, err := expr.Run(program, env)
|
|
||||||
if err != nil {
|
|
||||||
msg := fmt.Errorf("ошибка выполнения правила: %s, err: %v", validate.Rule, err)
|
|
||||||
logrus.Errorf("%s %v", prefix, msg)
|
|
||||||
return msg
|
|
||||||
}
|
|
||||||
if boolValue, ok := result.(bool); ok && !boolValue {
|
|
||||||
return fmt.Errorf("%s", validate.Message)
|
|
||||||
} else if !ok {
|
|
||||||
msg := fmt.Errorf("возвращаемое значение правила: %s не булево", validate.Rule)
|
|
||||||
logrus.Errorf("%s %v", prefix, msg)
|
|
||||||
return msg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateField создает функцию типа textinput.ValidateFunc для проверки значения полей
|
|
||||||
func ValidateField(fieldName, variableName string, validations []types.FieldValidate, app *types.PackageApplication) textinput.ValidateFunc {
|
|
||||||
if len(validations) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return func(text string) error {
|
|
||||||
env := ParamsForCompute(app)
|
|
||||||
env[variableName] = text
|
|
||||||
options := append([]expr.Option{
|
|
||||||
expr.Env(env),
|
|
||||||
expr.AllowUndefinedVariables(),
|
|
||||||
}, DefineExprCustomFunctions()...)
|
|
||||||
for _, validate := range validations {
|
|
||||||
program, err := expr.Compile(validate.Rule, options...)
|
|
||||||
prefix := fmt.Sprintf("Валидация поля %s,", fieldName)
|
|
||||||
if err != nil {
|
|
||||||
msg := fmt.Errorf("ошибка компиляции правила: %s, err: %v", validate.Rule, err)
|
|
||||||
logrus.Errorf("%s %v", prefix, msg)
|
|
||||||
return msg
|
|
||||||
}
|
|
||||||
result, err := expr.Run(program, env)
|
|
||||||
if err != nil {
|
|
||||||
msg := fmt.Errorf("ошибка выполнения правила: %s, err: %v", validate.Rule, err)
|
|
||||||
logrus.Errorf("%s %v", prefix, msg)
|
|
||||||
return msg
|
|
||||||
}
|
|
||||||
if boolValue, ok := result.(bool); ok && !boolValue {
|
|
||||||
return fmt.Errorf("%s", validate.Message)
|
|
||||||
} else if !ok {
|
|
||||||
msg := fmt.Errorf("возвращаемое значение правила: %s не булево", validate.Rule)
|
|
||||||
logrus.Errorf("%s %v", prefix, msg)
|
|
||||||
return msg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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, fmt.Errorf("no such directory or file")
|
|
||||||
} 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, fmt.Errorf("no such directory or file")
|
|
||||||
} 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),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SpecialValueHandle получаем необычные значения полей
|
|
||||||
func SpecialValueHandle(special string) interface{} {
|
|
||||||
specialValue := strings.Split(special, "code:")[1]
|
|
||||||
switch specialValue {
|
|
||||||
case "users":
|
|
||||||
return GetSystemUsers()
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetSystemUsers получение списка обычных пользователей в системе
|
|
||||||
func GetSystemUsers() []string {
|
|
||||||
//результат выполнения команды читающей файл passwd
|
|
||||||
output, _ := exec.Command("getent", "passwd").Output()
|
|
||||||
//массив строк разделенных новой строкой
|
|
||||||
lines := strings.Split(string(output), "\n")
|
|
||||||
var users []string
|
|
||||||
for _, line := range lines {
|
|
||||||
//линия совпадает с регулярным выражением
|
|
||||||
if matches := userRE.FindStringSubmatch(line); len(matches) == 3 {
|
|
||||||
//идентификатор больше или равен 1000, uid обычного пользователя в системе
|
|
||||||
if uid, _ := strconv.Atoi(matches[2]); uid >= 1000 {
|
|
||||||
users = append(users, matches[1])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return users
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExecuteAction выполняет указанное действие с использованием шаблонов из expr и картой параметров
|
|
||||||
func ExecuteAction(action *types.SectionAction, application *types.PackageApplication) error {
|
|
||||||
//Карта параметров
|
|
||||||
vars := ParamsForCondition(application)
|
|
||||||
switch action.Type {
|
|
||||||
case "command":
|
|
||||||
//Формируем строку команды по шаблону с подставленными значениями и передаем её для выполнения
|
|
||||||
cmdStr, cmdErr := utils.RenderTemplate(action.Command, vars)
|
|
||||||
if cmdErr != nil {
|
|
||||||
return fmt.Errorf("не удалось создать шаблон команды: %v", cmdErr)
|
|
||||||
}
|
|
||||||
return utils.RunCommand(cmdStr, action.RunAs)
|
|
||||||
case "file_template":
|
|
||||||
//Формируем 2 строки пути, с подставленными значениями и передаем её для выполнения
|
|
||||||
source, sourceErr := utils.RenderTemplate(action.FileSource, vars)
|
|
||||||
if sourceErr != nil {
|
|
||||||
return fmt.Errorf("не удалось создать шаблон файла источника: %v", sourceErr)
|
|
||||||
}
|
|
||||||
target, targetErr := utils.RenderTemplate(action.FileTarget, vars)
|
|
||||||
if targetErr != nil {
|
|
||||||
return fmt.Errorf("не удалось создать шаблон файла цели: %v", targetErr)
|
|
||||||
}
|
|
||||||
return utils.RenderFileTemplate(source, target, vars)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("действие для выполнения '%s' не опознано", action.Type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -37,8 +37,8 @@ func (m *CheckboxModel) Update(msg tea.Msg) (FieldModel, tea.Cmd) {
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
//Отслеживает нажатия клавиш.
|
//Отслеживает нажатия клавиш.
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case " ", "enter":
|
case "enter":
|
||||||
//Если был нажат enter или space, то меняем текущее значение на противоположное
|
//Если был нажат enter, то меняем текущее значение на противоположное
|
||||||
m.checked = !m.checked
|
m.checked = !m.checked
|
||||||
m.CheckboxValidate()
|
m.CheckboxValidate()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -138,7 +138,7 @@ func (m *DirSelectModel) Update(msg tea.Msg) (FieldModel, tea.Cmd) {
|
||||||
cmds = append(cmds, cmd)
|
cmds = append(cmds, cmd)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return m, tea.Batch(cmds...)
|
return m, tea.Sequence(cmds...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *DirSelectModel) View() string {
|
func (m *DirSelectModel) View() string {
|
||||||
|
|
|
||||||
|
|
@ -139,7 +139,7 @@ func (m *FileSelectModel) Update(msg tea.Msg) (FieldModel, tea.Cmd) {
|
||||||
cmds = append(cmds, cmd)
|
cmds = append(cmds, cmd)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return m, tea.Batch(cmds...)
|
return m, tea.Sequence(cmds...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *FileSelectModel) View() string {
|
func (m *FileSelectModel) View() string {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package setup
|
||||||
import (
|
import (
|
||||||
"bubble/setup/fields"
|
"bubble/setup/fields"
|
||||||
"bubble/setup/ui"
|
"bubble/setup/ui"
|
||||||
|
"bubble/setup/utils"
|
||||||
"bubble/types"
|
"bubble/types"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/charmbracelet/bubbles/viewport"
|
"github.com/charmbracelet/bubbles/viewport"
|
||||||
|
|
@ -31,7 +32,7 @@ func NewFormModel(application *types.PackageApplication, sectionIdx, focus, widt
|
||||||
//Заполняем массив моделей полей с учетом условий
|
//Заполняем массив моделей полей с учетом условий
|
||||||
var fieldModels []fields.FieldModel
|
var fieldModels []fields.FieldModel
|
||||||
for _, sectionField := range currentSection.Fields {
|
for _, sectionField := range currentSection.Fields {
|
||||||
if !CheckCondition(sectionField.Condition, application) {
|
if !utils.CheckCondition(sectionField.Condition, application) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
switch sectionField.FieldType {
|
switch sectionField.FieldType {
|
||||||
|
|
@ -41,13 +42,13 @@ func NewFormModel(application *types.PackageApplication, sectionIdx, focus, widt
|
||||||
options = append(options, fmt.Sprintf("%v", sectionField.Options[j]))
|
options = append(options, fmt.Sprintf("%v", sectionField.Options[j]))
|
||||||
}
|
}
|
||||||
if sectionField.SpecialValue != "" {
|
if sectionField.SpecialValue != "" {
|
||||||
specialOptions := SpecialValueHandle(sectionField.SpecialValue)
|
specialOptions := utils.SpecialValueHandle(sectionField.SpecialValue)
|
||||||
if strSpecial, ok := specialOptions.([]string); ok {
|
if strSpecial, ok := specialOptions.([]string); ok {
|
||||||
options = strSpecial
|
options = strSpecial
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
selected := fmt.Sprintf("%v", sectionField.Variable.Value)
|
selected := fmt.Sprintf("%v", sectionField.Variable.Value)
|
||||||
validationFunc := ValidateField(sectionField.Name, sectionField.VariableName, sectionField.Validate, application)
|
validationFunc := utils.ValidateField(sectionField.Name, sectionField.VariableName, sectionField.Validate, application)
|
||||||
fieldModels = append(fieldModels, fields.NewSelectModel(sectionField.Name, selected, sectionField.Description, options, validationFunc))
|
fieldModels = append(fieldModels, fields.NewSelectModel(sectionField.Name, selected, sectionField.Description, options, validationFunc))
|
||||||
case "check":
|
case "check":
|
||||||
checked := false
|
checked := false
|
||||||
|
|
@ -56,23 +57,23 @@ func NewFormModel(application *types.PackageApplication, sectionIdx, focus, widt
|
||||||
} else {
|
} else {
|
||||||
checked = sectionField.Variable.Value == "true"
|
checked = sectionField.Variable.Value == "true"
|
||||||
}
|
}
|
||||||
validationFunc := ValidateCheckField(sectionField.Name, sectionField.VariableName, sectionField.Validate, application)
|
validationFunc := utils.ValidateCheckField(sectionField.Name, sectionField.VariableName, sectionField.Validate, application)
|
||||||
fieldModels = append(fieldModels, fields.NewCheckboxModel(sectionField.Name, sectionField.Description, checked, validationFunc))
|
fieldModels = append(fieldModels, fields.NewCheckboxModel(sectionField.Name, sectionField.Description, checked, validationFunc))
|
||||||
case "compute":
|
case "compute":
|
||||||
value, err := ComputeValue(sectionField.Compute, application)
|
value, err := utils.ComputeValue(sectionField.Compute, application)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
value = fmt.Sprintf("Ошибка вычисления: %v", err)
|
value = fmt.Sprintf("Ошибка вычисления: %v", err)
|
||||||
}
|
}
|
||||||
fieldModels = append(fieldModels, fields.NewComputeModel(sectionField.Name, fmt.Sprintf("%v", value), sectionField.Description))
|
fieldModels = append(fieldModels, fields.NewComputeModel(sectionField.Name, fmt.Sprintf("%v", value), sectionField.Description))
|
||||||
//compute расчет
|
//compute расчет
|
||||||
case "file":
|
case "file":
|
||||||
validationFunc := ValidateField(sectionField.Name, sectionField.VariableName, sectionField.Validate, application)
|
validationFunc := utils.ValidateField(sectionField.Name, sectionField.VariableName, sectionField.Validate, application)
|
||||||
fieldModels = append(fieldModels, fields.NewFileSelectModel(sectionField.Name, fmt.Sprintf("%v", sectionField.Variable.Value), sectionField.Description, sectionField.FileFieldExtensions, validationFunc))
|
fieldModels = append(fieldModels, fields.NewFileSelectModel(sectionField.Name, fmt.Sprintf("%v", sectionField.Variable.Value), sectionField.Description, sectionField.FileFieldExtensions, validationFunc))
|
||||||
case "directory":
|
case "directory":
|
||||||
validationFunc := ValidateField(sectionField.Name, sectionField.VariableName, sectionField.Validate, application)
|
validationFunc := utils.ValidateField(sectionField.Name, sectionField.VariableName, sectionField.Validate, application)
|
||||||
fieldModels = append(fieldModels, fields.NewDirSelectModel(sectionField.Name, fmt.Sprintf("%v", sectionField.Variable.Value), sectionField.Description, validationFunc))
|
fieldModels = append(fieldModels, fields.NewDirSelectModel(sectionField.Name, fmt.Sprintf("%v", sectionField.Variable.Value), sectionField.Description, validationFunc))
|
||||||
default:
|
default:
|
||||||
validationFunc := ValidateField(sectionField.Name, sectionField.VariableName, sectionField.Validate, application)
|
validationFunc := utils.ValidateField(sectionField.Name, sectionField.VariableName, sectionField.Validate, application)
|
||||||
fieldModels = append(fieldModels, fields.NewInputModel(sectionField.Name, fmt.Sprintf("%v", sectionField.Variable.Value), sectionField.Description, validationFunc))
|
fieldModels = append(fieldModels, fields.NewInputModel(sectionField.Name, fmt.Sprintf("%v", sectionField.Variable.Value), sectionField.Description, validationFunc))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -165,7 +166,7 @@ func (m *FormModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
// фокус равен количество полей, значит он на кнопке следующей секции,
|
// фокус равен количество полей, значит он на кнопке следующей секции,
|
||||||
// если меньше количества полей или равен 0, значит фокус на поле формы.
|
// если меньше количества полей или равен 0, значит фокус на поле формы.
|
||||||
// Если у нас нет полей на форме (секция About), значит количество полей равно 0, а фокус по умолчанию устанавливается 0.
|
// Если у нас нет полей на форме (секция About), значит количество полей равно 0, а фокус по умолчанию устанавливается 0.
|
||||||
// Чтобы m.FieldModels[m.focus] вызвало ошибку "индекс вне массива",
|
// Чтобы m.FieldModels[m.focus] не вызвало ошибку "индекс вне массива",
|
||||||
// нужно чтобы в условие m.focus < len(m.FieldModels) попал фокус меньше 0 (-1,-2 и тд).
|
// нужно чтобы в условие m.focus < len(m.FieldModels) попал фокус меньше 0 (-1,-2 и тд).
|
||||||
// Чтобы этого избежать, есть проверка, которая устанавливает m.focus на последнюю позицию (количество полей на форме).
|
// Чтобы этого избежать, есть проверка, которая устанавливает m.focus на последнюю позицию (количество полей на форме).
|
||||||
// Чтобы эта проверка дала сбой, нужно после неё прописать изменение фокуса формы на отрицательный принудительно, так как такое не может произойти в текущем коде.
|
// Чтобы эта проверка дала сбой, нужно после неё прописать изменение фокуса формы на отрицательный принудительно, так как такое не может произойти в текущем коде.
|
||||||
|
|
@ -222,7 +223,7 @@ func (m *FormModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
var viewportCmd, fieldsCmd tea.Cmd
|
var viewportCmd, fieldsCmd tea.Cmd
|
||||||
m.viewport, viewportCmd = m.viewport.Update(msg)
|
m.viewport, viewportCmd = m.viewport.Update(msg)
|
||||||
fieldsCmd = m.updateFields(msg)
|
fieldsCmd = m.updateFields(msg)
|
||||||
return m, tea.Batch(viewportCmd, fieldsCmd)
|
return m, tea.Sequence(viewportCmd, fieldsCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *FormModel) View() string {
|
func (m *FormModel) View() string {
|
||||||
|
|
@ -407,5 +408,5 @@ func (m *FormModel) updateFields(msg tea.Msg) tea.Cmd {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//Объединяем весь массив команду в одну
|
//Объединяем весь массив команду в одну
|
||||||
return tea.Batch(cmds...)
|
return tea.Sequence(cmds...)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
99
setup/help.go
Normal file
99
setup/help.go
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
package setup
|
||||||
|
|
||||||
|
import "github.com/charmbracelet/bubbles/key"
|
||||||
|
|
||||||
|
//todo интегрировать нормально клавиши
|
||||||
|
|
||||||
|
// MainKeyMap структура главных клавиш
|
||||||
|
type MainKeyMap struct {
|
||||||
|
Help key.Binding //Клавиша помощи
|
||||||
|
Confirm key.Binding //Клавиша подтверждения
|
||||||
|
Exit key.Binding //Клавиша выхода
|
||||||
|
}
|
||||||
|
|
||||||
|
// ArrowKeyMap структура навигации
|
||||||
|
type ArrowKeyMap struct {
|
||||||
|
Up key.Binding //Клавиша перемещения наверх
|
||||||
|
Down key.Binding //Клавиша перемещения вниз
|
||||||
|
Left key.Binding //Клавиша перемещения влево
|
||||||
|
Right key.Binding //Клавиша перемещения вниз
|
||||||
|
Top key.Binding //Клавиша перемещения в самый верх
|
||||||
|
Bottom key.Binding //Клавиша перемещения в самый низ
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormKeyMap структура клавиш формы
|
||||||
|
type FormKeyMap struct {
|
||||||
|
MainKeyMap //Главные клавиши
|
||||||
|
ArrowKeyMap //Клавиши навигации
|
||||||
|
LogWindow key.Binding //Клавиша окна логирования
|
||||||
|
Close key.Binding //Клавиша закрытия
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListKeyMap структура клавиш списка приложений
|
||||||
|
type ListKeyMap struct {
|
||||||
|
MainKeyMap //Главные клавиши
|
||||||
|
ArrowKeyMap //Клавиши навигации
|
||||||
|
}
|
||||||
|
|
||||||
|
type BaseKeyMap struct {
|
||||||
|
Up key.Binding
|
||||||
|
Down key.Binding
|
||||||
|
Left key.Binding
|
||||||
|
Right key.Binding
|
||||||
|
Confirm key.Binding
|
||||||
|
Help key.Binding
|
||||||
|
Close key.Binding
|
||||||
|
Quit key.Binding
|
||||||
|
Log key.Binding
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k BaseKeyMap) ShortHelp() []key.Binding {
|
||||||
|
return []key.Binding{k.Help, k.Log, k.Close, k.Quit}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k BaseKeyMap) FullHelp() [][]key.Binding {
|
||||||
|
return [][]key.Binding{
|
||||||
|
{k.Up, k.Down, k.Right, k.Left},
|
||||||
|
{k.Confirm, k.Help, k.Log},
|
||||||
|
{k.Close, k.Quit},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var MainKeys = MainKeyMap{
|
||||||
|
Help: key.NewBinding(key.WithKeys("?", "ctrl+h"), key.WithHelp("?/ctrl+h", "help")),
|
||||||
|
Confirm: key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "confirm")),
|
||||||
|
Exit: key.NewBinding(key.WithKeys("ctrl+c"), key.WithHelp("ctrl+c", "exit")),
|
||||||
|
}
|
||||||
|
|
||||||
|
var ArrowKeys = ArrowKeyMap{
|
||||||
|
Up: key.NewBinding(key.WithKeys("up"), key.WithHelp("up", "move up")),
|
||||||
|
Down: key.NewBinding(key.WithKeys("down"), key.WithHelp("down", "move down")),
|
||||||
|
Left: key.NewBinding(key.WithKeys("left"), key.WithHelp("left", "move left")),
|
||||||
|
Right: key.NewBinding(key.WithKeys("right"), key.WithHelp("right", "move right")),
|
||||||
|
Top: key.NewBinding(key.WithKeys("home"), key.WithHelp("home", "to the top")),
|
||||||
|
Bottom: key.NewBinding(key.WithKeys("end"), key.WithHelp("end", "to the end")),
|
||||||
|
}
|
||||||
|
|
||||||
|
var FormKeys = FormKeyMap{
|
||||||
|
MainKeyMap: MainKeys,
|
||||||
|
ArrowKeyMap: ArrowKeys,
|
||||||
|
LogWindow: key.NewBinding(key.WithKeys("ctrl+l"), key.WithHelp("ctrl+l", "show/hide logs")),
|
||||||
|
Close: key.NewBinding(key.WithKeys("esc"), key.WithHelp("esc", "close select/close app setting")),
|
||||||
|
}
|
||||||
|
|
||||||
|
var ListKeys = ListKeyMap{
|
||||||
|
MainKeyMap: MainKeys,
|
||||||
|
ArrowKeyMap: ArrowKeys,
|
||||||
|
}
|
||||||
|
|
||||||
|
var keys = BaseKeyMap{
|
||||||
|
Up: key.NewBinding(key.WithKeys("up"), key.WithHelp("up", "move up")),
|
||||||
|
Down: key.NewBinding(key.WithKeys("down"), key.WithHelp("down", "move down")),
|
||||||
|
Left: key.NewBinding(key.WithKeys("left"), key.WithHelp("left", "move left")),
|
||||||
|
Right: key.NewBinding(key.WithKeys("right"), key.WithHelp("right", "move right")),
|
||||||
|
Confirm: key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "confirm")),
|
||||||
|
Help: key.NewBinding(key.WithKeys("?", "ctrl+h"), key.WithHelp("?/ctrl+h", "help")),
|
||||||
|
Close: key.NewBinding(key.WithKeys("esc"), key.WithHelp("esc", "close")),
|
||||||
|
Quit: key.NewBinding(key.WithKeys("ctrl+c"), key.WithHelp("ctrl+c", "quit")),
|
||||||
|
Log: key.NewBinding(key.WithKeys("ctrl+l"), key.WithHelp("ctrl+l", "show/hide log window")),
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"bubble/setup/ui"
|
"bubble/setup/ui"
|
||||||
"bubble/types"
|
"bubble/types"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/charmbracelet/bubbles/key"
|
||||||
"github.com/charmbracelet/bubbles/list"
|
"github.com/charmbracelet/bubbles/list"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
|
@ -76,9 +77,9 @@ func (m *ListApplicationsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
return m, nil
|
return m, nil
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
//Отслеживаем нажатия клавиш списка
|
//Отслеживаем нажатия клавиш списка
|
||||||
switch msg.String() {
|
switch {
|
||||||
case "enter":
|
case key.Matches(msg, keys.Confirm):
|
||||||
//Обработчик enter нужен для подтверждения элемента списка.
|
//Обработчик выбора нужен для подтверждения элемента списка.
|
||||||
//Получаем информацию о выбранном приложении,
|
//Получаем информацию о выбранном приложении,
|
||||||
//возвращаем команду по возврату сообщения о показе модального окна подтверждения настройки приложения.
|
//возвращаем команду по возврату сообщения о показе модального окна подтверждения настройки приложения.
|
||||||
selected := m.List.SelectedItem().(ListApplicationItem)
|
selected := m.List.SelectedItem().(ListApplicationItem)
|
||||||
|
|
@ -102,6 +103,7 @@ func (m *ListApplicationsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
//Если ранее не было возвращено команд, то передаем сообщение напрямую в список
|
//Если ранее не было возвращено команд, то передаем сообщение напрямую в список
|
||||||
m.List, cmd = m.List.Update(msg)
|
m.List, cmd = m.List.Update(msg)
|
||||||
|
|
|
||||||
213
setup/main.go
213
setup/main.go
|
|
@ -2,23 +2,25 @@ package setup
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bubble/bootstrap"
|
"bubble/bootstrap"
|
||||||
|
"bubble/setup/utils"
|
||||||
"bubble/types"
|
"bubble/types"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/charmbracelet/bubbles/help"
|
||||||
|
"github.com/charmbracelet/bubbles/key"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"os/exec"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Start() {
|
func Start() {
|
||||||
|
//Создаем новый объект Main для настройки.
|
||||||
m := NewMain()
|
m := NewMain()
|
||||||
p := tea.NewProgram(m, tea.WithAltScreen())
|
p := tea.NewProgram(m, tea.WithAltScreen()) //Запускаем через bubble tea программу
|
||||||
m.p = p
|
m.Program = p //Указываем объекту настроек, программу в которой запущен, чтобы передавать туда сигналы
|
||||||
if _, err := p.Run(); err != nil {
|
if _, err := p.Run(); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo подсказка горячие клавиши
|
|
||||||
// Main структура описывающая объект настроек
|
// Main структура описывающая объект настроек
|
||||||
type Main struct {
|
type Main struct {
|
||||||
current tea.Model //Текущая модель для отображения
|
current tea.Model //Текущая модель для отображения
|
||||||
|
|
@ -31,7 +33,9 @@ type Main struct {
|
||||||
CurrentSectionIdx int //Индекс текущей секции
|
CurrentSectionIdx int //Индекс текущей секции
|
||||||
Width int //Ширина окна
|
Width int //Ширина окна
|
||||||
Height int //Высота окна
|
Height int //Высота окна
|
||||||
p *tea.Program
|
Program *tea.Program //Программа для отправки данных между собой
|
||||||
|
Keys BaseKeyMap
|
||||||
|
help help.Model
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMain создаем новый объект настройки
|
// NewMain создаем новый объект настройки
|
||||||
|
|
@ -41,7 +45,10 @@ func NewMain() *Main {
|
||||||
m := &Main{
|
m := &Main{
|
||||||
Applications: apps,
|
Applications: apps,
|
||||||
CurrentSectionIdx: 0,
|
CurrentSectionIdx: 0,
|
||||||
|
Keys: keys,
|
||||||
|
help: help.New(),
|
||||||
}
|
}
|
||||||
|
m.help.ShowAll = false
|
||||||
//Записываем в текущую модель список приложений
|
//Записываем в текущую модель список приложений
|
||||||
m.current = NewListApplicationsModel(m.Applications, m.Width, m.Height)
|
m.current = NewListApplicationsModel(m.Applications, m.Width, m.Height)
|
||||||
m.log = NewLogModel(m.Width, m.Height)
|
m.log = NewLogModel(m.Width, m.Height)
|
||||||
|
|
@ -56,8 +63,9 @@ func (m *Main) Init() tea.Cmd {
|
||||||
func (m *Main) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (m *Main) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
var cmds []tea.Cmd
|
var cmds []tea.Cmd
|
||||||
var cmd tea.Cmd
|
var cmd tea.Cmd
|
||||||
//Если у нас установлен модель модального окна, то передаем сообщение в его функцию обновления,
|
//Если у нас установлена модель модального окна, то передаем сообщение в его функцию обновления,
|
||||||
//при возврате команды модального окна записываем её в массив команд.
|
//при возврате команды модального окна записываем её в массив команд.
|
||||||
|
//Если у нас сейчас показывается окно логирования, то обновляем его модель.
|
||||||
//В противном случае передаем сообщение в функцию обновления текущей модели,
|
//В противном случае передаем сообщение в функцию обновления текущей модели,
|
||||||
//при возврате команды также записываем её в массив команд.
|
//при возврате команды также записываем её в массив команд.
|
||||||
if m.overlay != nil {
|
if m.overlay != nil {
|
||||||
|
|
@ -65,7 +73,7 @@ func (m *Main) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
if cmd != nil {
|
if cmd != nil {
|
||||||
cmds = append(cmds, cmd)
|
cmds = append(cmds, cmd)
|
||||||
}
|
}
|
||||||
} else if m.showLog { //todo комментарии
|
} else if m.showLog {
|
||||||
m.log, cmd = m.log.Update(msg)
|
m.log, cmd = m.log.Update(msg)
|
||||||
if cmd != nil {
|
if cmd != nil {
|
||||||
cmds = append(cmds, cmd)
|
cmds = append(cmds, cmd)
|
||||||
|
|
@ -92,12 +100,15 @@ func (m *Main) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
}
|
}
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
//Отслеживаем нажатие клавиш приложения
|
//Отслеживаем нажатие клавиш приложения
|
||||||
switch msg.String() {
|
switch {
|
||||||
case "ctrl+l":
|
case key.Matches(msg, m.Keys.Help):
|
||||||
//Обработчик клавиш ctrl+l показывает/скрывает окно логирования действий
|
//Обработчик клавиш помощи, показывает/скрывает полное меню подсказок
|
||||||
|
m.help.ShowAll = !m.help.ShowAll
|
||||||
|
case key.Matches(msg, m.Keys.Log):
|
||||||
|
//Обработчик клавиш логов показывает/скрывает окно логирования действий
|
||||||
m.showLog = !m.showLog
|
m.showLog = !m.showLog
|
||||||
case "ctrl+c":
|
case key.Matches(msg, m.Keys.Quit):
|
||||||
//Обработчик клавиш ctrl+c у приложения нужен для его закрытия.
|
//Обработчик клавиш закрытия приложения.
|
||||||
//Возвращаем команду по возврату сообщения о показе модального окна
|
//Возвращаем команду по возврату сообщения о показе модального окна
|
||||||
return m, func() tea.Msg {
|
return m, func() tea.Msg {
|
||||||
return types.ShowModalMsg{
|
return types.ShowModalMsg{
|
||||||
|
|
@ -119,19 +130,28 @@ func (m *Main) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
}
|
}
|
||||||
case types.NextSectionMsg:
|
case types.NextSectionMsg:
|
||||||
//Получаем сообщение о переходе на следующую секцию настраиваемого приложения.
|
//Получаем сообщение о переходе на следующую секцию настраиваемого приложения.
|
||||||
//Добавляем команду для возврата сообщения показа модального окна о выполнении действий секции
|
//Если у текущей секции есть действия, то добавляем в массив команд функцию возврата сообщения показа модального окна с информацией о запуске действий.
|
||||||
//todo добавить проверку на наличие действий, если действий нет, просто переходить к следующей секции, выдавать сообщение "Действий секции нет"
|
//Запускаем функцию выполнения действий секции в горутине.
|
||||||
cmds = append(cmds, func() tea.Msg {
|
//Если действий у текущей секции нет, то просто меняем секцию на следующую
|
||||||
return types.ShowModalMsg{
|
if len(m.CurrentSection.Actions) > 0 {
|
||||||
Type: types.ModalInfo,
|
cmds = append(cmds, func() tea.Msg {
|
||||||
Title: "Выполнение действий",
|
return types.ShowModalMsg{
|
||||||
Message: "Ожидайте завершения действий секции",
|
Type: types.ModalLoading,
|
||||||
ModalButtons: nil,
|
Title: "Выполнение действий",
|
||||||
OnResult: nil,
|
Message: "Ожидайте завершения действий секции",
|
||||||
|
ModalButtons: nil,
|
||||||
|
OnResult: nil,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
//Запуск функции выполнения действий в горутине
|
||||||
|
go m.executeSectionActions()
|
||||||
|
} else {
|
||||||
|
if ok, modalMsg := m.NextSectionProcess(); !ok {
|
||||||
|
return m, func() tea.Msg {
|
||||||
|
return modalMsg
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
//Запуск функции выполнения действий в горутине
|
|
||||||
go m.executeSectionActions()
|
|
||||||
case types.ActionsCompleteMsg:
|
case types.ActionsCompleteMsg:
|
||||||
//Команда закрытия модального окна
|
//Команда закрытия модального окна
|
||||||
cmds = append(cmds, func() tea.Msg {
|
cmds = append(cmds, func() tea.Msg {
|
||||||
|
|
@ -142,45 +162,12 @@ func (m *Main) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
logModel.logs = append(logModel.logs, msg.Log...)
|
logModel.logs = append(logModel.logs, msg.Log...)
|
||||||
m.log, cmd = logModel.Update(msg)
|
m.log, cmd = logModel.Update(msg)
|
||||||
if msg.Success {
|
if msg.Success {
|
||||||
//Если действие успешно, переходим к следующей секции.
|
//Если действие успешно, то запускаем функцию перехода на следующую секцию.
|
||||||
//Ищем индекс следующей секции, если он остался 0, то следующие секции недоступны.
|
if ok, modalMsg := m.NextSectionProcess(); !ok {
|
||||||
//Возвращаем модальное окно с выходом к списку приложений
|
|
||||||
nextSectionIndex := 0
|
|
||||||
for sectionIdx := m.CurrentSectionIdx + 1; sectionIdx < len(m.CurrentApplication.Sections); sectionIdx++ {
|
|
||||||
section := m.CurrentApplication.Sections[sectionIdx]
|
|
||||||
if CheckCondition(section.Condition, m.CurrentApplication) {
|
|
||||||
nextSectionIndex = sectionIdx
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if nextSectionIndex == 0 {
|
|
||||||
return m, func() tea.Msg {
|
return m, func() tea.Msg {
|
||||||
return types.ShowModalMsg{
|
return modalMsg
|
||||||
Type: types.ModalWarning,
|
|
||||||
Title: "Ошибка секций приложения",
|
|
||||||
Message: "Указанное приложение не имеет больше доступных секций для обработки",
|
|
||||||
ModalButtons: []types.ModalButton{{
|
|
||||||
Text: "Вернутся",
|
|
||||||
Value: true,
|
|
||||||
}},
|
|
||||||
OnResult: func(result any) tea.Cmd {
|
|
||||||
return func() tea.Msg {
|
|
||||||
return types.BackToListMsg{}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Если следующая секция доступна записываем секцию и её индекс.
|
|
||||||
//Выводим либо финальную форму, если секция последняя и её тип SectionFinal, либо следующую простую секцию.
|
|
||||||
m.CurrentSectionIdx = nextSectionIndex
|
|
||||||
m.CurrentSection = &m.CurrentApplication.Sections[nextSectionIndex]
|
|
||||||
if nextSectionIndex == len(m.CurrentApplication.Sections)-1 && m.CurrentSection.Type == types.SectionFinal {
|
|
||||||
m.current = NewFinalModel(m.CurrentApplication.Variables, m.Width, m.Height)
|
|
||||||
} else {
|
|
||||||
m.current = NewFormModel(m.CurrentApplication, m.CurrentSectionIdx, 0, m.Width, m.Height)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
//Если действие не удалось выполнить показываем модель логов принудительно,
|
//Если действие не удалось выполнить показываем модель логов принудительно,
|
||||||
//также возвращаем команду возврата сообщения показа модального окна
|
//также возвращаем команду возврата сообщения показа модального окна
|
||||||
|
|
@ -207,7 +194,7 @@ func (m *Main) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
//ищем доступную секцию
|
//ищем доступную секцию
|
||||||
for sectionIdx := 0; sectionIdx < len(m.CurrentApplication.Sections); sectionIdx++ {
|
for sectionIdx := 0; sectionIdx < len(m.CurrentApplication.Sections); sectionIdx++ {
|
||||||
section := m.CurrentApplication.Sections[sectionIdx]
|
section := m.CurrentApplication.Sections[sectionIdx]
|
||||||
if CheckCondition(section.Condition, m.CurrentApplication) {
|
if utils.CheckCondition(section.Condition, m.CurrentApplication) {
|
||||||
m.CurrentSectionIdx = sectionIdx
|
m.CurrentSectionIdx = sectionIdx
|
||||||
m.CurrentSection = §ion
|
m.CurrentSection = §ion
|
||||||
break
|
break
|
||||||
|
|
@ -243,19 +230,24 @@ func (m *Main) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
case types.ShowModalMsg:
|
case types.ShowModalMsg:
|
||||||
//Получаем сообщение о показе модального окна.
|
//Получаем сообщение о показе модального окна.
|
||||||
//В поле модального окна записываем новый объект модального окна на основе сообщений.
|
//В поле модального окна записываем новый объект модального окна на основе сообщений.
|
||||||
|
//Инициализируем модальное окно, если оно вернуло команду, добавляем её в массив.
|
||||||
//НЕ СТИРАЕМ ТЕКУЩЕЕ ПРИЛОЖЕНИЕ
|
//НЕ СТИРАЕМ ТЕКУЩЕЕ ПРИЛОЖЕНИЕ
|
||||||
m.overlay = NewModalModel(msg, m.Width, m.Height)
|
m.overlay = NewModalModel(msg, m.Width, m.Height)
|
||||||
return m, nil
|
initModalCmd := m.overlay.Init()
|
||||||
|
if initModalCmd != nil {
|
||||||
|
cmds = append(cmds, initModalCmd)
|
||||||
|
}
|
||||||
|
return m, tea.Sequence(cmds...)
|
||||||
case types.CloseModalMsg:
|
case types.CloseModalMsg:
|
||||||
//Получаем сообщение о закрытии модального окна.
|
//Получаем сообщение о закрытии модального окна.
|
||||||
//Стираем модальное окно.
|
//Стираем модальное окно.
|
||||||
m.overlay = nil
|
m.overlay = nil
|
||||||
return m, nil
|
return m, nil
|
||||||
//предохранитель
|
//предохранитель
|
||||||
//return m,tea.Batch(cmds...)
|
//return m,tea.Sequence(cmds...)
|
||||||
}
|
}
|
||||||
//Возвращаем команду собранную из массива команд текущей модели
|
//Возвращаем команду собранную из массива команд текущей модели
|
||||||
return m, tea.Batch(cmds...)
|
return m, tea.Sequence(cmds...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// View формирует строку на основе модального окна или текущей модели
|
// View формирует строку на основе модального окна или текущей модели
|
||||||
|
|
@ -267,55 +259,22 @@ func (m *Main) View() string {
|
||||||
if m.overlay != nil {
|
if m.overlay != nil {
|
||||||
view = m.overlay.View()
|
view = m.overlay.View()
|
||||||
}
|
}
|
||||||
return view
|
return view + "\n\n" + m.help.View(m.Keys)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sugar проверяет поле указанное в сообщение на важность в форме и приложении.
|
// Sugar проверяет поле указанное в сообщение на важность в форме и приложении.
|
||||||
func (m *Main) Sugar(sugar types.SugarMsg) {
|
func (m *Main) Sugar(sugar types.SugarMsg) {
|
||||||
//Если полей с указанной переменной несколько, то выключаем все прочие поля кроме, указанного в сообщении
|
//Если полей с указанной переменной несколько, то выключаем все прочие поля кроме, указанного в сообщении
|
||||||
if m.IsMultiField(sugar.VariableName) {
|
if utils.IsMultiField(sugar.VariableName, m.CurrentApplication.Sections) {
|
||||||
m.DeactivateField(sugar.VariableName, sugar.FieldName)
|
m.DeactivateField(sugar.VariableName, sugar.FieldName)
|
||||||
}
|
}
|
||||||
//Если от переменной поля зависит секции или поля,
|
//Если от переменной поля зависит секции или поля,
|
||||||
//то формируем новую форму с учетом прошлого фокуса
|
//то формируем новую форму с учетом прошлого фокуса
|
||||||
if IsParent(sugar.VariableName, m.CurrentApplication.Sections) {
|
if utils.IsParent(sugar.VariableName, m.CurrentApplication.Sections) {
|
||||||
m.current = NewFormModel(m.CurrentApplication, m.CurrentSectionIdx, sugar.Focus, m.Width, m.Height)
|
m.current = NewFormModel(m.CurrentApplication, m.CurrentSectionIdx, sugar.Focus, m.Width, m.Height)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsParent проверяет, является ли указанная переменная условием для поля или секции
|
|
||||||
func IsParent(variableName string, sections []types.Section) bool {
|
|
||||||
for _, section := range sections {
|
|
||||||
for _, parent := range section.Parent {
|
|
||||||
if parent == variableName {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, field := range section.Fields {
|
|
||||||
for _, parent := range field.Parent {
|
|
||||||
if parent == variableName {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsMultiField проверяет переменную на использование несколькими полями.
|
|
||||||
func (m *Main) IsMultiField(variableName string) bool {
|
|
||||||
var count int
|
|
||||||
for _, section := range m.CurrentApplication.Sections {
|
|
||||||
for _, field := range section.Fields {
|
|
||||||
if field.Name == variableName {
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return count > 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeactivateField отключает все поля использующие указанную переменную, кроме поля с указанным именем
|
// DeactivateField отключает все поля использующие указанную переменную, кроме поля с указанным именем
|
||||||
func (m *Main) DeactivateField(variableName, fieldName string) {
|
func (m *Main) DeactivateField(variableName, fieldName string) {
|
||||||
for sectionIdx, section := range m.CurrentApplication.Sections {
|
for sectionIdx, section := range m.CurrentApplication.Sections {
|
||||||
|
|
@ -340,14 +299,14 @@ func (m *Main) executeSectionActions() {
|
||||||
logBuffer = append(logBuffer, sectionLog)
|
logBuffer = append(logBuffer, sectionLog)
|
||||||
for _, action := range m.CurrentSection.Actions {
|
for _, action := range m.CurrentSection.Actions {
|
||||||
//Если условие для действия не выполняется, то пропускаем
|
//Если условие для действия не выполняется, то пропускаем
|
||||||
if !CheckCondition(action.Condition, m.CurrentApplication) {
|
if !utils.CheckCondition(action.Condition, m.CurrentApplication) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
sectionActionLog := fmt.Sprintf("Выполнение действия: %s", action.Name)
|
sectionActionLog := fmt.Sprintf("Выполнение действия: %s", action.Name)
|
||||||
logrus.Infof(sectionActionLog)
|
logrus.Infof(sectionActionLog)
|
||||||
logBuffer = append(logBuffer, sectionActionLog)
|
logBuffer = append(logBuffer, sectionActionLog)
|
||||||
//Проверяем, что все нужные пакеты для действия установлены
|
//Проверяем, что все нужные пакеты для действия установлены
|
||||||
if err := m.InstallPackage(action.RequirePackages); err != nil {
|
if err := utils.InstallPackages(action.RequirePackages); err != nil {
|
||||||
errLog := fmt.Sprintf("Ошибка установки пакета: %v", err)
|
errLog := fmt.Sprintf("Ошибка установки пакета: %v", err)
|
||||||
logrus.Errorf(errLog)
|
logrus.Errorf(errLog)
|
||||||
logBuffer = append(logBuffer, errLog)
|
logBuffer = append(logBuffer, errLog)
|
||||||
|
|
@ -355,7 +314,7 @@ func (m *Main) executeSectionActions() {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
//Выполняем действие
|
//Выполняем действие
|
||||||
if err := ExecuteAction(&action, m.CurrentApplication); err != nil {
|
if err := utils.ExecuteAction(&action, m.CurrentApplication); err != nil {
|
||||||
errLog := fmt.Sprintf("Ошибка выполнения действия: %v", err)
|
errLog := fmt.Sprintf("Ошибка выполнения действия: %v", err)
|
||||||
logrus.Errorf(errLog)
|
logrus.Errorf(errLog)
|
||||||
logBuffer = append(logBuffer, errLog)
|
logBuffer = append(logBuffer, errLog)
|
||||||
|
|
@ -367,23 +326,47 @@ func (m *Main) executeSectionActions() {
|
||||||
logrus.Infof(successMsg)
|
logrus.Infof(successMsg)
|
||||||
}
|
}
|
||||||
//Отправляем в программу сигнал об успешном завершении действия
|
//Отправляем в программу сигнал об успешном завершении действия
|
||||||
m.p.Send(types.ActionsCompleteMsg{
|
m.Program.Send(types.ActionsCompleteMsg{
|
||||||
Success: success,
|
Success: success,
|
||||||
Log: logBuffer,
|
Log: logBuffer,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// InstallPackage проверяет установлен ли пакет, устанавливает при отсутствии
|
// NextSectionProcess проверяет доступность следующей секции для настройки
|
||||||
func (m *Main) InstallPackage(packages []string) error {
|
func (m *Main) NextSectionProcess() (bool, types.ShowModalMsg) {
|
||||||
for _, pkg := range packages {
|
//Ищем индекс следующей секции, если он остался 0, то следующие секции недоступны.
|
||||||
cmd := exec.Command("dpkg", "-s", pkg)
|
//Возвращаем модальное окно с выходом к списку приложений
|
||||||
if err := cmd.Run(); err != nil {
|
nextSectionIndex := 0
|
||||||
installCmd := exec.Command("apt", "install", "-y", pkg)
|
for sectionIDx := m.CurrentSectionIdx + 1; sectionIDx < len(m.CurrentApplication.Sections); sectionIDx++ {
|
||||||
out, err := installCmd.CombinedOutput()
|
section := m.CurrentApplication.Sections[sectionIDx]
|
||||||
if err != nil {
|
if utils.CheckCondition(section.Condition, m.CurrentApplication) {
|
||||||
return fmt.Errorf("пакет %s: %v\nOutput: %s", pkg, err, out)
|
nextSectionIndex = sectionIDx
|
||||||
}
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
if nextSectionIndex == 0 {
|
||||||
|
return false, types.ShowModalMsg{
|
||||||
|
Type: types.ModalWarning,
|
||||||
|
Title: "Ошибка секций приложения",
|
||||||
|
Message: "Указанное приложение не имеет больше доступных секций для обработки",
|
||||||
|
ModalButtons: []types.ModalButton{
|
||||||
|
{Text: "Вернуться", Value: true},
|
||||||
|
},
|
||||||
|
OnResult: func(result any) tea.Cmd {
|
||||||
|
return func() tea.Msg {
|
||||||
|
return types.BackToListMsg{}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//Если следующая секция доступна записываем секцию и её индекс.
|
||||||
|
//Выводим либо финальную форму, если секция последняя и её тип SectionFinal, либо следующую простую секцию.
|
||||||
|
m.CurrentSectionIdx = nextSectionIndex
|
||||||
|
m.CurrentSection = &m.CurrentApplication.Sections[nextSectionIndex]
|
||||||
|
if nextSectionIndex == len(m.CurrentApplication.Sections)-1 && m.CurrentSection.Type == types.SectionFinal {
|
||||||
|
m.current = NewFinalModel(m.CurrentApplication.Variables, m.Width, m.Height)
|
||||||
|
} else {
|
||||||
|
m.current = NewFormModel(m.CurrentApplication, m.CurrentSectionIdx, 0, m.Width, m.Height)
|
||||||
|
}
|
||||||
|
return true, types.ShowModalMsg{}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package setup
|
||||||
import (
|
import (
|
||||||
"bubble/setup/ui"
|
"bubble/setup/ui"
|
||||||
"bubble/types"
|
"bubble/types"
|
||||||
|
"github.com/charmbracelet/bubbles/spinner"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
)
|
)
|
||||||
|
|
@ -17,11 +18,12 @@ type ModalModel struct {
|
||||||
width int //Ширина окна
|
width int //Ширина окна
|
||||||
height int //Высота окна
|
height int //Высота окна
|
||||||
onResult func(result any) tea.Cmd //Какое действие надо выполнить при нажатии
|
onResult func(result any) tea.Cmd //Какое действие надо выполнить при нажатии
|
||||||
|
spinner spinner.Model //Для демонстрации прогресса выполнения действий
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewModalModel создает новую модель модального окна на основе переданного сообщения
|
// NewModalModel создает новую модель модального окна на основе переданного сообщения
|
||||||
func NewModalModel(msg types.ShowModalMsg, width, height int) *ModalModel {
|
func NewModalModel(msg types.ShowModalMsg, width, height int) *ModalModel {
|
||||||
return &ModalModel{
|
m := &ModalModel{
|
||||||
modalType: msg.Type,
|
modalType: msg.Type,
|
||||||
title: msg.Title,
|
title: msg.Title,
|
||||||
message: msg.Message,
|
message: msg.Message,
|
||||||
|
|
@ -31,14 +33,25 @@ func NewModalModel(msg types.ShowModalMsg, width, height int) *ModalModel {
|
||||||
height: height,
|
height: height,
|
||||||
onResult: msg.OnResult,
|
onResult: msg.OnResult,
|
||||||
}
|
}
|
||||||
|
if msg.Type == types.ModalLoading {
|
||||||
|
s := spinner.New()
|
||||||
|
s.Spinner = spinner.Line
|
||||||
|
s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
|
||||||
|
m.spinner = s
|
||||||
|
}
|
||||||
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *ModalModel) Init() tea.Cmd {
|
func (m *ModalModel) Init() tea.Cmd {
|
||||||
|
if m.modalType == types.ModalLoading {
|
||||||
|
return m.spinner.Tick
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update отслеживает действия с модальным окном
|
// Update отслеживает действия с модальным окном
|
||||||
func (m *ModalModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (m *ModalModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
var cmd tea.Cmd
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case tea.WindowSizeMsg:
|
case tea.WindowSizeMsg:
|
||||||
//Отслеживаем и записываем новые размеры окна
|
//Отслеживаем и записываем новые размеры окна
|
||||||
|
|
@ -64,24 +77,28 @@ func (m *ModalModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
if m.focus >= len(m.buttons) {
|
if m.focus >= len(m.buttons) {
|
||||||
m.focus = 0
|
m.focus = 0
|
||||||
}
|
}
|
||||||
case "enter", " ":
|
case "enter":
|
||||||
//Обработчик клавиши enter нужен для подтверждения кнопки.
|
//Обработчик клавиши enter нужен для подтверждения кнопки.
|
||||||
//Получаем значение кнопки, а после если у модального окна прописана функция,
|
//Если у модального окна есть кнопки.
|
||||||
|
//Получаем значение нажатой кнопки, а после если у модального окна прописана функция,
|
||||||
//то передаем значение кнопки в функцию модального окна.
|
//то передаем значение кнопки в функцию модального окна.
|
||||||
result := m.buttons[m.focus].Value
|
if len(m.buttons) > 0 {
|
||||||
var cmd tea.Cmd
|
result := m.buttons[m.focus].Value
|
||||||
if m.onResult != nil {
|
if m.onResult != nil {
|
||||||
cmd = m.onResult(result)
|
cmd = m.onResult(result)
|
||||||
|
}
|
||||||
|
//Возвращаем единую команду составленную из двух,
|
||||||
|
//результата выполнения функции модального окна,
|
||||||
|
//функцию возвращения сообщения о закрытии модального окна.
|
||||||
|
return m, tea.Sequence(cmd, func() tea.Msg { return types.CloseModalMsg{} })
|
||||||
}
|
}
|
||||||
//Возвращаем единую команду составленную из двух,
|
|
||||||
//результата выполнения функции модального окна,
|
|
||||||
//функцию возвращения сообщения о закрытии модального окна.
|
|
||||||
return m, tea.Batch(
|
|
||||||
cmd,
|
|
||||||
func() tea.Msg { return types.CloseModalMsg{} },
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//Если это модальное окно загрузки, то обновляем счетчик
|
||||||
|
if m.modalType == types.ModalLoading {
|
||||||
|
m.spinner, cmd = m.spinner.Update(msg)
|
||||||
|
return m, cmd
|
||||||
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -106,7 +123,9 @@ func (m *ModalModel) View() string {
|
||||||
buttonRow := lipgloss.JoinHorizontal(lipgloss.Center, buttons...)
|
buttonRow := lipgloss.JoinHorizontal(lipgloss.Center, buttons...)
|
||||||
//Тело контента модального окна
|
//Тело контента модального окна
|
||||||
modalBody := lipgloss.JoinVertical(lipgloss.Center, message, "", buttonRow)
|
modalBody := lipgloss.JoinVertical(lipgloss.Center, message, "", buttonRow)
|
||||||
|
if m.modalType == types.ModalLoading {
|
||||||
|
modalBody = lipgloss.JoinVertical(lipgloss.Center, m.spinner.View(), message)
|
||||||
|
}
|
||||||
//В зависимости от типа модального окна разные цвета
|
//В зависимости от типа модального окна разные цвета
|
||||||
var borderColor lipgloss.Color
|
var borderColor lipgloss.Color
|
||||||
var modalTitleStyle lipgloss.Style
|
var modalTitleStyle lipgloss.Style
|
||||||
|
|
|
||||||
160
setup/utils/actions.go
Normal file
160
setup/utils/actions.go
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bubble/types"
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"html/template"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InstallPackages устанавливает требуемые пакеты если их нет в системе
|
||||||
|
func InstallPackages(requiredPackages []string) error {
|
||||||
|
for _, requiredPackage := range requiredPackages {
|
||||||
|
if result, checkError := IsPackageInstall(requiredPackage); !result {
|
||||||
|
if checkError != nil {
|
||||||
|
return fmt.Errorf("пакет %s: %v", requiredPackage, checkError)
|
||||||
|
}
|
||||||
|
installCmd := exec.Command("apt", "install", "-y", requiredPackage)
|
||||||
|
out, err := installCmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("пакет %s: %v\nOutput: %s", requiredPackage, err, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsPackageInstall проверяет установлен ли пакет в системе
|
||||||
|
func IsPackageInstall(packageName string) (bool, error) {
|
||||||
|
cmd := exec.Command("dpkg", "-s", packageName)
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
var exitError *exec.ExitError
|
||||||
|
if errors.As(err, &exitError) && exitError.ExitCode() == 1 {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecuteAction выполняет указанное действие с использованием шаблонов из expr и картой параметров
|
||||||
|
func ExecuteAction(action *types.SectionAction, application *types.PackageApplication) error {
|
||||||
|
//Карта параметров
|
||||||
|
vars := ParamsForCondition(application)
|
||||||
|
switch action.Type {
|
||||||
|
case "command":
|
||||||
|
//Формируем строку команды по шаблону с подставленными значениями и передаем её для выполнения
|
||||||
|
cmdStr, cmdErr := RenderTemplate(action.Command, vars)
|
||||||
|
if cmdErr != nil {
|
||||||
|
return fmt.Errorf("не удалось создать шаблон команды: %v", cmdErr)
|
||||||
|
}
|
||||||
|
return RunCommand(cmdStr, action.RunAs)
|
||||||
|
case "file_template":
|
||||||
|
//Формируем 2 строки пути, с подставленными значениями и передаем её для выполнения
|
||||||
|
source, sourceErr := RenderTemplate(action.FileSource, vars)
|
||||||
|
if sourceErr != nil {
|
||||||
|
return fmt.Errorf("не удалось создать шаблон файла источника: %v", sourceErr)
|
||||||
|
}
|
||||||
|
target, targetErr := RenderTemplate(action.FileTarget, vars)
|
||||||
|
if targetErr != nil {
|
||||||
|
return fmt.Errorf("не удалось создать шаблон файла цели: %v", targetErr)
|
||||||
|
}
|
||||||
|
return RenderFileTemplate(source, target, vars)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("действие для выполнения '%s' не опознано", action.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunCommand запускает выполнение переданной команды от указанного лица
|
||||||
|
func RunCommand(cmdStr, runAs string) error {
|
||||||
|
if cmdStr == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
if runAs != "" && runAs != "root" {
|
||||||
|
cmd = exec.Command("su", "-c", cmdStr, runAs)
|
||||||
|
} else {
|
||||||
|
cmd = exec.Command("bash", "-c", cmdStr)
|
||||||
|
}
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ошибка команды: %v\nВывод: %s", err, string(output))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderFileTemplate создает целевой файл на основе файла источника с подставленными значениями в нем
|
||||||
|
func RenderFileTemplate(source, target string, vars map[string]interface{}) error {
|
||||||
|
if source == "" || target == "" {
|
||||||
|
return fmt.Errorf("source или target пустые")
|
||||||
|
}
|
||||||
|
var data []byte
|
||||||
|
var err error
|
||||||
|
//Получаем данные файла источника
|
||||||
|
if strings.HasPrefix(source, "http") {
|
||||||
|
resp, err := http.Get(source)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ошибка получения данных файла по адресу %s, err : %v", source, err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
data, err = io.ReadAll(resp.Body)
|
||||||
|
} else {
|
||||||
|
data, err = os.ReadFile(source)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ошибка чтения данных файла %s, err: %v", source, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Формируем файл с переменными
|
||||||
|
tmpl, err := template.New("").Parse(string(data))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ошибка формирования шаблона строки: %v", err)
|
||||||
|
}
|
||||||
|
var rendered bytes.Buffer
|
||||||
|
|
||||||
|
//В файл помещаем переменные
|
||||||
|
if err = tmpl.Execute(&rendered, vars); err != nil {
|
||||||
|
return fmt.Errorf("ошибка подстановки значений в шаблон")
|
||||||
|
}
|
||||||
|
if _, err = os.Stat(target); err == nil {
|
||||||
|
backup := target + ".bak" + time.Now().Format("20060102-150405")
|
||||||
|
if cpErr := CopyFile(target, backup); cpErr != nil {
|
||||||
|
logrus.Warnf("Не удалось создать бекап: %v", cpErr)
|
||||||
|
} else {
|
||||||
|
logrus.Infof("Бекап %s создан", backup)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//Создаем папку для целевого файла и записываем в него данные
|
||||||
|
if err = os.MkdirAll(filepath.Dir(target), 0755); err != nil {
|
||||||
|
return fmt.Errorf("ошибка создания директории для файла %s, err: %v", target, err)
|
||||||
|
}
|
||||||
|
if err = os.WriteFile(target, rendered.Bytes(), 0644); err != nil {
|
||||||
|
return fmt.Errorf("ошибка записи в файл %s, err: %v", target, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderTemplate формирует строку с подставленными параметрами
|
||||||
|
func RenderTemplate(tmpl string, data map[string]interface{}) (string, error) {
|
||||||
|
if tmpl == "" {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
t, err := template.New("").Parse(tmpl)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("ошибка формирования шаблона строки: %v", err)
|
||||||
|
}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err = t.Execute(&buf, data); err != nil {
|
||||||
|
return "", fmt.Errorf("ошибка подстановки значений в шаблон: %v", err)
|
||||||
|
}
|
||||||
|
return buf.String(), nil
|
||||||
|
}
|
||||||
221
setup/utils/field.go
Normal file
221
setup/utils/field.go
Normal file
|
|
@ -0,0 +1,221 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bubble/types"
|
||||||
|
"fmt"
|
||||||
|
"github.com/charmbracelet/bubbles/textinput"
|
||||||
|
"github.com/expr-lang/expr"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"os/exec"
|
||||||
|
"reflect"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// userRE - с начала строки, делит все по знаку двоеточия, берется первые 3 группы, пример user_name:x:1000:
|
||||||
|
var userRE = regexp.MustCompile(`^([^:]+):[^:]*:([0-9]+):`)
|
||||||
|
|
||||||
|
// ComputeValue вычисляет значение для поля compute.
|
||||||
|
func ComputeValue(computeExpr string, app *types.PackageApplication) (interface{}, error) {
|
||||||
|
if computeExpr == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
env := ParamsForCompute(app)
|
||||||
|
options := append([]expr.Option{
|
||||||
|
expr.Env(env),
|
||||||
|
expr.AsKind(reflect.String), // по умолчанию строка, если не указано иное
|
||||||
|
expr.AllowUndefinedVariables(),
|
||||||
|
}, DefineExprCustomFunctions()...)
|
||||||
|
|
||||||
|
program, err := expr.Compile(computeExpr, options...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ошибка компиляции compute: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := expr.Run(program, env)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ошибка выполнения compute: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recompute производит перерасчет всех полей
|
||||||
|
func Recompute(app *types.PackageApplication) {
|
||||||
|
for i := range app.Sections {
|
||||||
|
section := &app.Sections[i]
|
||||||
|
for j := range section.Fields {
|
||||||
|
field := §ion.Fields[j]
|
||||||
|
if field.FieldType == "compute" && field.Compute != "" {
|
||||||
|
value, err := ComputeValue(field.Compute, app)
|
||||||
|
if err != nil {
|
||||||
|
value = fmt.Sprintf("Ошибка вычисления: %v", err)
|
||||||
|
}
|
||||||
|
field.Variable.Value = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckCondition проверяет равно ли переданное условие
|
||||||
|
func CheckCondition(condition string, app *types.PackageApplication) bool {
|
||||||
|
if condition == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := expr.Eval(condition, ParamsForCondition(app))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("Ошибка в условии '%s': %v", condition, err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
boolResult, ok := result.(bool)
|
||||||
|
if !ok {
|
||||||
|
logrus.Warnf("Условие '%s' не вернуло bool, а %T: %v", condition, result, result)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return boolResult
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsParent проверяет, является ли указанная переменная условием для поля или секции
|
||||||
|
func IsParent(variableName string, sections []types.Section) bool {
|
||||||
|
for _, section := range sections {
|
||||||
|
for _, parent := range section.Parent {
|
||||||
|
if parent == variableName {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, field := range section.Fields {
|
||||||
|
for _, parent := range field.Parent {
|
||||||
|
if parent == variableName {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsMultiField проверяет переменную на использование несколькими полями.
|
||||||
|
func IsMultiField(variableName string, applicationSections []types.Section) bool {
|
||||||
|
var count int
|
||||||
|
for _, section := range applicationSections {
|
||||||
|
for _, field := range section.Fields {
|
||||||
|
if field.Name == variableName {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count > 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// SpecialValueHandle получаем необычные значения полей
|
||||||
|
func SpecialValueHandle(special string) interface{} {
|
||||||
|
specialValue := strings.Split(special, "code:")[1]
|
||||||
|
switch specialValue {
|
||||||
|
case "users":
|
||||||
|
return GetSystemUsers()
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSystemUsers получение списка обычных пользователей в системе
|
||||||
|
func GetSystemUsers() []string {
|
||||||
|
//результат выполнения команды читающей файл passwd
|
||||||
|
output, _ := exec.Command("getent", "passwd").Output()
|
||||||
|
//массив строк разделенных новой строкой
|
||||||
|
lines := strings.Split(string(output), "\n")
|
||||||
|
var users []string
|
||||||
|
for _, line := range lines {
|
||||||
|
//линия совпадает с регулярным выражением
|
||||||
|
if matches := userRE.FindStringSubmatch(line); len(matches) == 3 {
|
||||||
|
//идентификатор больше или равен 1000, uid обычного пользователя в системе
|
||||||
|
if uid, _ := strconv.Atoi(matches[2]); uid >= 1000 {
|
||||||
|
users = append(users, matches[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return users
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateCheckField создает функцию типа types.BoolValidateFunc для проверки значения bool полей
|
||||||
|
func ValidateCheckField(fieldName, variableName string, validations []types.FieldValidate, app *types.PackageApplication) types.BoolValidateFunc {
|
||||||
|
if len(validations) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return func(checked bool) error {
|
||||||
|
env := ParamsForCompute(app)
|
||||||
|
env[variableName] = checked
|
||||||
|
options := append([]expr.Option{
|
||||||
|
expr.Env(env),
|
||||||
|
expr.AllowUndefinedVariables(),
|
||||||
|
}, DefineExprCustomFunctions()...)
|
||||||
|
for _, validate := range validations {
|
||||||
|
program, err := expr.Compile(validate.Rule, options...)
|
||||||
|
prefix := fmt.Sprintf("Валидация поля %s,", fieldName)
|
||||||
|
if err != nil {
|
||||||
|
msg := fmt.Errorf("ошибка компиляции правила: %s, err: %v", validate.Rule, err)
|
||||||
|
logrus.Errorf("%s %v", prefix, msg)
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
result, err := expr.Run(program, env)
|
||||||
|
if err != nil {
|
||||||
|
msg := fmt.Errorf("ошибка выполнения правила: %s, err: %v", validate.Rule, err)
|
||||||
|
logrus.Errorf("%s %v", prefix, msg)
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
if boolValue, ok := result.(bool); ok && !boolValue {
|
||||||
|
return fmt.Errorf("%s", validate.Message)
|
||||||
|
} else if !ok {
|
||||||
|
msg := fmt.Errorf("возвращаемое значение правила: %s не булево", validate.Rule)
|
||||||
|
logrus.Errorf("%s %v", prefix, msg)
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateField создает функцию типа textinput.ValidateFunc для проверки значения text полей
|
||||||
|
func ValidateField(fieldName, variableName string, validations []types.FieldValidate, app *types.PackageApplication) textinput.ValidateFunc {
|
||||||
|
if len(validations) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return func(text string) error {
|
||||||
|
env := ParamsForCompute(app)
|
||||||
|
env[variableName] = text
|
||||||
|
options := append([]expr.Option{
|
||||||
|
expr.Env(env),
|
||||||
|
expr.AllowUndefinedVariables(),
|
||||||
|
}, DefineExprCustomFunctions()...)
|
||||||
|
for _, validate := range validations {
|
||||||
|
program, err := expr.Compile(validate.Rule, options...)
|
||||||
|
prefix := fmt.Sprintf("Валидация поля %s,", fieldName)
|
||||||
|
if err != nil {
|
||||||
|
msg := fmt.Errorf("ошибка компиляции правила: %s, err: %v", validate.Rule, err)
|
||||||
|
logrus.Errorf("%s %v", prefix, msg)
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
result, err := expr.Run(program, env)
|
||||||
|
if err != nil {
|
||||||
|
msg := fmt.Errorf("ошибка выполнения правила: %s, err: %v", validate.Rule, err)
|
||||||
|
logrus.Errorf("%s %v", prefix, msg)
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
if boolValue, ok := result.(bool); ok && !boolValue {
|
||||||
|
return fmt.Errorf("%s", validate.Message)
|
||||||
|
} else if !ok {
|
||||||
|
msg := fmt.Errorf("возвращаемое значение правила: %s не булево", validate.Rule)
|
||||||
|
logrus.Errorf("%s %v", prefix, msg)
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,17 +1,18 @@
|
||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bubble/types"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/expr-lang/expr"
|
||||||
"html/template"
|
|
||||||
"io"
|
"io"
|
||||||
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// CalculateSelectOptionsIndex рассчитывает индекс первой и последней опции для отображения в поле выбора
|
// CalculateSelectOptionsIndex рассчитывает индекс первой и последней опции для отображения в поле выбора
|
||||||
|
|
@ -44,91 +45,6 @@ func CalculateSelectOptionsIndex(focus, visibleCount, optionsCount int) (int, in
|
||||||
return start, end
|
return start, end
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenderFileTemplate создает целевой файл на основе файла источника с подставленными значениями в нем
|
|
||||||
func RenderFileTemplate(source, target string, vars map[string]interface{}) error {
|
|
||||||
if source == "" || target == "" {
|
|
||||||
return fmt.Errorf("source или target пустые")
|
|
||||||
}
|
|
||||||
var data []byte
|
|
||||||
var err error
|
|
||||||
//Получаем данные файла источника
|
|
||||||
if strings.HasPrefix(source, "http") {
|
|
||||||
resp, err := http.Get(source)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("ошибка получения данных файла по адресу %s, err : %v", source, err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
data, err = io.ReadAll(resp.Body)
|
|
||||||
} else {
|
|
||||||
data, err = os.ReadFile(source)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("ошибка чтения данных файла %s, err: %v", source, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
//Формируем файл с переменными
|
|
||||||
tmpl, err := template.New("").Parse(string(data))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("ошибка формирования шаблона строки: %v", err)
|
|
||||||
}
|
|
||||||
var rendered bytes.Buffer
|
|
||||||
|
|
||||||
//В файл помещаем переменные
|
|
||||||
if err = tmpl.Execute(&rendered, vars); err != nil {
|
|
||||||
return fmt.Errorf("ошибка подстановки значений в шаблон")
|
|
||||||
}
|
|
||||||
if _, err = os.Stat(target); err == nil {
|
|
||||||
backup := target + ".bak" + time.Now().Format("20060102-150405")
|
|
||||||
if cpErr := CopyFile(target, backup); cpErr != nil {
|
|
||||||
logrus.Warnf("Не удалось создать бекап: %v", cpErr)
|
|
||||||
} else {
|
|
||||||
logrus.Infof("Бекап %s создан", backup)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//Создаем папку для целевого файла и записываем в него данные
|
|
||||||
if err = os.MkdirAll(filepath.Dir(target), 0755); err != nil {
|
|
||||||
return fmt.Errorf("ошибка создания директории для файла %s, err: %v", target, err)
|
|
||||||
}
|
|
||||||
if err = os.WriteFile(target, rendered.Bytes(), 0644); err != nil {
|
|
||||||
return fmt.Errorf("ошибка записи в файл %s, err: %v", target, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RenderTemplate формирует строку с подставленными параметрами
|
|
||||||
func RenderTemplate(tmpl string, data map[string]interface{}) (string, error) {
|
|
||||||
if tmpl == "" {
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
t, err := template.New("").Parse(tmpl)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("ошибка формирования шаблона строки: %v", err)
|
|
||||||
}
|
|
||||||
var buf bytes.Buffer
|
|
||||||
if err = t.Execute(&buf, data); err != nil {
|
|
||||||
return "", fmt.Errorf("ошибка подстановки значений в шаблон: %v", err)
|
|
||||||
}
|
|
||||||
return buf.String(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RunCommand запускает выполнение переданной команды от указанного лица
|
|
||||||
func RunCommand(cmdStr, runAs string) error {
|
|
||||||
if cmdStr == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
var cmd *exec.Cmd
|
|
||||||
if runAs != "" && runAs != "root" {
|
|
||||||
cmd = exec.Command("su", "-c", cmdStr, runAs)
|
|
||||||
} else {
|
|
||||||
cmd = exec.Command("bash", "-c", cmdStr)
|
|
||||||
}
|
|
||||||
output, err := cmd.CombinedOutput()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("ошибка команды: %v\nВывод: %s", err, string(output))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CopyFile копирование файла в новое место
|
// CopyFile копирование файла в новое место
|
||||||
func CopyFile(src, dst string) error {
|
func CopyFile(src, dst string) error {
|
||||||
in, err := os.Open(src)
|
in, err := os.Open(src)
|
||||||
|
|
@ -145,3 +61,247 @@ func CopyFile(src, dst string) error {
|
||||||
_, err = io.Copy(out, in)
|
_, err = io.Copy(out, in)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParamsForCompute получение словаря переменных без приведения типов, для вычисления
|
||||||
|
func ParamsForCompute(app *types.PackageApplication) map[string]interface{} {
|
||||||
|
params := make(map[string]interface{})
|
||||||
|
|
||||||
|
for i := range app.Variables {
|
||||||
|
v := &app.Variables[i]
|
||||||
|
value := v.Value
|
||||||
|
if value == nil {
|
||||||
|
value = v.Default
|
||||||
|
}
|
||||||
|
params[v.Name] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем значения из активных полей
|
||||||
|
for _, section := range app.Sections {
|
||||||
|
for _, field := range section.Fields {
|
||||||
|
if field.Variable == nil || !field.Active {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
value := field.Variable.Value
|
||||||
|
if value == nil {
|
||||||
|
value = field.Variable.Default
|
||||||
|
}
|
||||||
|
params[field.VariableName] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParamsForCondition получение словаря переменных с приведением типов для проверки условий
|
||||||
|
func ParamsForCondition(app *types.PackageApplication) map[string]interface{} {
|
||||||
|
params := make(map[string]interface{})
|
||||||
|
|
||||||
|
for _, variable := range app.Variables {
|
||||||
|
value := variable.Value
|
||||||
|
if value == nil {
|
||||||
|
value = variable.Default
|
||||||
|
}
|
||||||
|
|
||||||
|
// Приводим к нужному типу, как в твоём старом коде
|
||||||
|
switch variable.Type {
|
||||||
|
case "bool":
|
||||||
|
if s, ok := value.(string); ok {
|
||||||
|
value = strings.ToLower(s) == "true"
|
||||||
|
}
|
||||||
|
case "number", "int", "float":
|
||||||
|
if s, ok := value.(string); ok {
|
||||||
|
if n, err := strconv.ParseFloat(s, 64); err == nil {
|
||||||
|
value = n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
params[variable.Name] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Также добавляем значения из полей секций (на случай, если переменная используется только в поле)
|
||||||
|
for _, section := range app.Sections {
|
||||||
|
for _, field := range section.Fields {
|
||||||
|
if field.Variable == nil || !field.Active {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
value := field.Variable.Value
|
||||||
|
if value == nil {
|
||||||
|
value = field.Variable.Default
|
||||||
|
}
|
||||||
|
|
||||||
|
switch field.Variable.Type {
|
||||||
|
case "bool":
|
||||||
|
if s, ok := value.(string); ok {
|
||||||
|
value = strings.ToLower(s) == "true"
|
||||||
|
}
|
||||||
|
case "number", "int", "float":
|
||||||
|
if s, ok := value.(string); ok {
|
||||||
|
if n, err := strconv.ParseFloat(s, 64); err == nil {
|
||||||
|
value = n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
params[field.VariableName] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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, fmt.Errorf("no such directory or file")
|
||||||
|
} 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, fmt.Errorf("no such directory or file")
|
||||||
|
} 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),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,4 +7,5 @@ const (
|
||||||
ModalWarning ModalType = "warning"
|
ModalWarning ModalType = "warning"
|
||||||
ModalError ModalType = "error"
|
ModalError ModalType = "error"
|
||||||
ModalConfirm ModalType = "confirm"
|
ModalConfirm ModalType = "confirm"
|
||||||
|
ModalLoading ModalType = "loading"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue