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

This commit is contained in:
XeRo 2026-01-23 17:52:47 +05:00
parent 91b1eea1d4
commit c69a7ae84b
14 changed files with 886 additions and 706 deletions

View file

@ -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: "Настройка данных пользователя"

View file

@ -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 := &section.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)
}
}

View file

@ -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()
} }

View file

@ -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 {

View file

@ -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 {

View file

@ -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
View 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")),
}

View file

@ -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)

View file

@ -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,11 +130,13 @@ func (m *Main) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} }
case types.NextSectionMsg: case types.NextSectionMsg:
//Получаем сообщение о переходе на следующую секцию настраиваемого приложения. //Получаем сообщение о переходе на следующую секцию настраиваемого приложения.
//Добавляем команду для возврата сообщения показа модального окна о выполнении действий секции //Если у текущей секции есть действия, то добавляем в массив команд функцию возврата сообщения показа модального окна с информацией о запуске действий.
//todo добавить проверку на наличие действий, если действий нет, просто переходить к следующей секции, выдавать сообщение "Действий секции нет" //Запускаем функцию выполнения действий секции в горутине.
//Если действий у текущей секции нет, то просто меняем секцию на следующую
if len(m.CurrentSection.Actions) > 0 {
cmds = append(cmds, func() tea.Msg { cmds = append(cmds, func() tea.Msg {
return types.ShowModalMsg{ return types.ShowModalMsg{
Type: types.ModalInfo, Type: types.ModalLoading,
Title: "Выполнение действий", Title: "Выполнение действий",
Message: "Ожидайте завершения действий секции", Message: "Ожидайте завершения действий секции",
ModalButtons: nil, ModalButtons: nil,
@ -132,6 +145,13 @@ func (m *Main) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}) })
//Запуск функции выполнения действий в горутине //Запуск функции выполнения действий в горутине
go m.executeSectionActions() go m.executeSectionActions()
} else {
if ok, modalMsg := m.NextSectionProcess(); !ok {
return m, func() tea.Msg {
return modalMsg
}
}
}
case types.ActionsCompleteMsg: case types.ActionsCompleteMsg:
//Команда закрытия модального окна //Команда закрытия модального окна
cmds = append(cmds, func() tea.Msg { cmds = append(cmds, func() tea.Msg {
@ -142,44 +162,11 @@ 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 = &section m.CurrentSection = &section
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
} }
} }
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{}
} }
return nil },
}
}
//Если следующая секция доступна записываем секцию и её индекс.
//Выводим либо финальную форму, если секция последняя и её тип 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{}
} }

View file

@ -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 нужен для подтверждения кнопки.
//Получаем значение кнопки, а после если у модального окна прописана функция, //Если у модального окна есть кнопки.
//Получаем значение нажатой кнопки, а после если у модального окна прописана функция,
//то передаем значение кнопки в функцию модального окна. //то передаем значение кнопки в функцию модального окна.
if len(m.buttons) > 0 {
result := m.buttons[m.focus].Value result := m.buttons[m.focus].Value
var cmd tea.Cmd
if m.onResult != nil { if m.onResult != nil {
cmd = m.onResult(result) cmd = m.onResult(result)
} }
//Возвращаем единую команду составленную из двух, //Возвращаем единую команду составленную из двух,
//результата выполнения функции модального окна, //результата выполнения функции модального окна,
//функцию возвращения сообщения о закрытии модального окна. //функцию возвращения сообщения о закрытии модального окна.
return m, tea.Batch( return m, tea.Sequence(cmd, func() tea.Msg { return types.CloseModalMsg{} })
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
View 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
View 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 := &section.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
}
}

View file

@ -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),
),
}
}

View file

@ -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"
) )