Добавление и исправление комментариев, начало добавление валидации, сообщение прокрути viewport на форме
This commit is contained in:
parent
c7ff7662ba
commit
e37cabadcc
14 changed files with 371 additions and 297 deletions
|
|
@ -159,6 +159,9 @@ sections:
|
||||||
variable-name: "is_secure"
|
variable-name: "is_secure"
|
||||||
field-type: "check"
|
field-type: "check"
|
||||||
description: "Использовать SSL"
|
description: "Использовать SSL"
|
||||||
|
validate:
|
||||||
|
- rule: "min(1,nginx_port)"
|
||||||
|
message: "Значение слишком большое"
|
||||||
- name: "server_address"
|
- name: "server_address"
|
||||||
variable-name: "server_address"
|
variable-name: "server_address"
|
||||||
field-type: "input"
|
field-type: "input"
|
||||||
|
|
@ -284,6 +287,9 @@ sections:
|
||||||
variable-name: "debian_root"
|
variable-name: "debian_root"
|
||||||
field-type: "directory"
|
field-type: "directory"
|
||||||
description: "Директория debian"
|
description: "Директория debian"
|
||||||
|
validate:
|
||||||
|
- rule: "dir_exists(debian_root)"
|
||||||
|
message: "Папка не существует"
|
||||||
- name: "finale"
|
- name: "finale"
|
||||||
description: "Последний этап, проверьте правильность введенных данных"
|
description: "Последний этап, проверьте правильность введенных данных"
|
||||||
type: "final"
|
type: "final"
|
||||||
|
|
@ -307,8 +313,8 @@ sections:
|
||||||
error: "Ошибка при создании файла repko.target.service"
|
error: "Ошибка при создании файла repko.target.service"
|
||||||
- name: "update_repko_ini"
|
- name: "update_repko_ini"
|
||||||
type: "file_template"
|
type: "file_template"
|
||||||
# file-source: "/home/test/Projects/Personal/GoSetup/_public/files/donor/config.donor.ini"
|
# file-source: "/home/test/Projects/Personal/GoSetup/_public/files/donor/config.donor.ini"
|
||||||
# file-target: "/home/test/Projects/Personal/GoSetup/_public/files/target/config.target.ini"
|
# file-target: "/home/test/Projects/Personal/GoSetup/_public/files/target/config.target.ini"
|
||||||
file-source: "http://gsg.site/_public/config.donor.ini"
|
file-source: "http://gsg.site/_public/config.donor.ini"
|
||||||
file-target: "/home/test/Projects/Personal/GoSetup/_public/files/target/http.config.ini"
|
file-target: "/home/test/Projects/Personal/GoSetup/_public/files/target/http.config.ini"
|
||||||
error: "Ошибка при создании файла repko.target.service"
|
error: "Ошибка при создании файла repko.target.service"
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,13 @@ package setup
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bubble/types"
|
"bubble/types"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/charmbracelet/bubbles/textinput"
|
||||||
"github.com/expr-lang/expr"
|
"github.com/expr-lang/expr"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"io"
|
"io"
|
||||||
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
|
@ -18,8 +21,7 @@ import (
|
||||||
// userRE - с начала строки, делит все по знаку двоеточия, берется первые 3 группы, пример user_name:x:1000:
|
// userRE - с начала строки, делит все по знаку двоеточия, берется первые 3 группы, пример user_name:x:1000:
|
||||||
var userRE = regexp.MustCompile(`^([^:]+):[^:]*:([0-9]+):`)
|
var userRE = regexp.MustCompile(`^([^:]+):[^:]*:([0-9]+):`)
|
||||||
|
|
||||||
// ParamsForCondition — собирает все текущие значения переменных приложения
|
// ParamsForCondition получение словаря переменных с приведением типов для проверки условий
|
||||||
// для использования в expr-условиях (condition)
|
|
||||||
func ParamsForCondition(app *types.PackageApplication) map[string]interface{} {
|
func ParamsForCondition(app *types.PackageApplication) map[string]interface{} {
|
||||||
params := make(map[string]interface{})
|
params := make(map[string]interface{})
|
||||||
|
|
||||||
|
|
@ -77,7 +79,7 @@ func ParamsForCondition(app *types.PackageApplication) map[string]interface{} {
|
||||||
return params
|
return params
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckCondition — проверяет условие visibility (condition) для секции или поля
|
// CheckCondition проверяет равно ли переданное условие
|
||||||
func CheckCondition(condition string, app *types.PackageApplication) bool {
|
func CheckCondition(condition string, app *types.PackageApplication) bool {
|
||||||
if condition == "" {
|
if condition == "" {
|
||||||
return true
|
return true
|
||||||
|
|
@ -99,7 +101,7 @@ func CheckCondition(condition string, app *types.PackageApplication) bool {
|
||||||
return boolResult
|
return boolResult
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParamsForCompute — аналогично, но только по переменным (как в твоём старом коде)
|
// ParamsForCompute получение словаря переменных без приведения типов, для вычисления
|
||||||
func ParamsForCompute(app *types.PackageApplication) map[string]interface{} {
|
func ParamsForCompute(app *types.PackageApplication) map[string]interface{} {
|
||||||
params := make(map[string]interface{})
|
params := make(map[string]interface{})
|
||||||
|
|
||||||
|
|
@ -129,7 +131,7 @@ func ParamsForCompute(app *types.PackageApplication) map[string]interface{} {
|
||||||
return params
|
return params
|
||||||
}
|
}
|
||||||
|
|
||||||
// ComputeValue — вычисляет значение для compute-поля
|
// ComputeValue вычисляет значение для поля compute.
|
||||||
func ComputeValue(computeExpr string, app *types.PackageApplication) (interface{}, error) {
|
func ComputeValue(computeExpr string, app *types.PackageApplication) (interface{}, error) {
|
||||||
if computeExpr == "" {
|
if computeExpr == "" {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
|
@ -172,6 +174,44 @@ func Recompute(app *types.PackageApplication) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 //todo text это строка, если использовать функцию типа textinput.ValidateFunc, то словим ошибку типов
|
||||||
|
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
|
// DefineExprCustomFunctions возвращает собственные функции expr
|
||||||
func DefineExprCustomFunctions() []expr.Option {
|
func DefineExprCustomFunctions() []expr.Option {
|
||||||
return []expr.Option{
|
return []expr.Option{
|
||||||
|
|
@ -270,8 +310,17 @@ func DefineExprCustomFunctions() []expr.Option {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
path := fmt.Sprintf("%v", args[0])
|
path := fmt.Sprintf("%v", args[0])
|
||||||
_, err := os.Stat(path)
|
fileInfo, err := os.Stat(path)
|
||||||
return err == nil, nil
|
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),
|
}, new(func(string) bool),
|
||||||
),
|
),
|
||||||
//проверяет наличие директории в системе
|
//проверяет наличие директории в системе
|
||||||
|
|
@ -282,13 +331,16 @@ func DefineExprCustomFunctions() []expr.Option {
|
||||||
}
|
}
|
||||||
path := fmt.Sprintf("%v", args[0])
|
path := fmt.Sprintf("%v", args[0])
|
||||||
fileInfo, err := os.Stat(path)
|
fileInfo, err := os.Stat(path)
|
||||||
if err != nil {
|
if err == nil && fileInfo.IsDir() {
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if fileInfo.IsDir() {
|
|
||||||
return true, nil
|
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
|
||||||
}
|
}
|
||||||
return false, nil
|
|
||||||
}, new(func(string) bool),
|
}, new(func(string) bool),
|
||||||
),
|
),
|
||||||
//проверяет доступность порта
|
//проверяет доступность порта
|
||||||
|
|
|
||||||
|
|
@ -2,47 +2,62 @@ package fields
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/charmbracelet/bubbles/textinput"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// CheckboxModel структура описывающая модель выбора из двух состояний да и нет
|
||||||
type CheckboxModel struct {
|
type CheckboxModel struct {
|
||||||
Label string
|
label string //Имя поля
|
||||||
description string
|
description string //Описание поля
|
||||||
Checked bool
|
checked bool //Выбранное состояние
|
||||||
|
validate textinput.ValidateFunc //Функция валидации
|
||||||
|
err error //Поле ошибки валидации
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCheckboxModel(label, description string, checked bool) *CheckboxModel {
|
// NewCheckboxModel создаем новую модель выбора
|
||||||
return &CheckboxModel{
|
func NewCheckboxModel(label, description string, checked bool, validator textinput.ValidateFunc) *CheckboxModel {
|
||||||
Label: label,
|
checkbox := &CheckboxModel{
|
||||||
|
label: label,
|
||||||
description: description,
|
description: description,
|
||||||
Checked: checked,
|
validate: validator,
|
||||||
|
checked: checked,
|
||||||
}
|
}
|
||||||
|
checkbox.err = validator(strconv.FormatBool(checked))
|
||||||
|
|
||||||
|
return checkbox
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *CheckboxModel) Init() tea.Cmd {
|
func (m *CheckboxModel) Init() tea.Cmd {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update отслеживает действия с полем
|
||||||
func (m *CheckboxModel) Update(msg tea.Msg) (FieldModel, tea.Cmd) {
|
func (m *CheckboxModel) Update(msg tea.Msg) (FieldModel, tea.Cmd) {
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
|
//Отслеживает нажатия клавиш.
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case " ", "enter":
|
case " ", "enter":
|
||||||
m.Checked = !m.Checked
|
//Если был нажат enter или space, то меняем текущее значение на противоположное
|
||||||
|
m.checked = !m.checked
|
||||||
|
m.err = m.validate(strconv.FormatBool(m.checked))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *CheckboxModel) View() string {
|
func (m *CheckboxModel) View() string {
|
||||||
res := ""
|
res := fmt.Sprintf("%s: ", m.label)
|
||||||
res += m.Label + ": "
|
if m.checked {
|
||||||
if m.Checked {
|
|
||||||
res += "[x]"
|
res += "[x]"
|
||||||
} else {
|
} else {
|
||||||
res += "[ ]"
|
res += "[ ]"
|
||||||
}
|
}
|
||||||
if m.description != "" {
|
if m.err != nil {
|
||||||
|
res += fmt.Sprintf("\nОшибка валидации: %s", m.err)
|
||||||
|
} else if m.description != "" {
|
||||||
res += fmt.Sprintf("\n%s", m.description)
|
res += fmt.Sprintf("\n%s", m.description)
|
||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
|
|
@ -62,9 +77,9 @@ func (m *CheckboxModel) CaptureKeys() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *CheckboxModel) GetLabel() string {
|
func (m *CheckboxModel) GetLabel() string {
|
||||||
return m.Label
|
return m.label
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *CheckboxModel) GetValue() interface{} {
|
func (m *CheckboxModel) GetValue() interface{} {
|
||||||
return m.Checked
|
return m.checked
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,15 +5,17 @@ import (
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ComputeModel структура описывающая модель вывода вычисляемых значений
|
||||||
type ComputeModel struct {
|
type ComputeModel struct {
|
||||||
Label string
|
label string //Имя поля
|
||||||
description string
|
description string //Описание поля
|
||||||
value string
|
value string //Значение поля
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewComputeModel создаем новую модель вывода вычисляемых значений
|
||||||
func NewComputeModel(label, value, description string) *ComputeModel {
|
func NewComputeModel(label, value, description string) *ComputeModel {
|
||||||
return &ComputeModel{
|
return &ComputeModel{
|
||||||
Label: label,
|
label: label,
|
||||||
description: description,
|
description: description,
|
||||||
value: value,
|
value: value,
|
||||||
}
|
}
|
||||||
|
|
@ -23,12 +25,13 @@ func (m *ComputeModel) Init() tea.Cmd {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update отслеживает действия с полем. Так как поле нужно только для вывода, никаких действий не выполняем.
|
||||||
func (m *ComputeModel) Update(msg tea.Msg) (FieldModel, tea.Cmd) {
|
func (m *ComputeModel) Update(msg tea.Msg) (FieldModel, tea.Cmd) {
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *ComputeModel) View() string {
|
func (m *ComputeModel) View() string {
|
||||||
res := fmt.Sprintf("%s: %s", m.Label, m.value)
|
res := fmt.Sprintf("%s: %s", m.label, m.value)
|
||||||
if m.description != "" {
|
if m.description != "" {
|
||||||
res += fmt.Sprintf("\n%s", m.description)
|
res += fmt.Sprintf("\n%s", m.description)
|
||||||
}
|
}
|
||||||
|
|
@ -46,7 +49,7 @@ func (m *ComputeModel) CaptureKeys() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *ComputeModel) GetLabel() string {
|
func (m *ComputeModel) GetLabel() string {
|
||||||
return m.Label
|
return m.label
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *ComputeModel) GetValue() interface{} {
|
func (m *ComputeModel) GetValue() interface{} {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package fields
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bubble/setup/ui"
|
"bubble/setup/ui"
|
||||||
|
"bubble/setup/utils"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/charmbracelet/bubbles/textinput"
|
"github.com/charmbracelet/bubbles/textinput"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
|
@ -11,20 +12,23 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// DirSelectModel структура описывающая модель поля выбора директории
|
||||||
type DirSelectModel struct {
|
type DirSelectModel struct {
|
||||||
input textinput.Model
|
input textinput.Model //Готовый компонент input для ввода данных
|
||||||
label string
|
label string //Имя поля
|
||||||
description string
|
description string //Описание поля
|
||||||
focus int
|
focus int //Фокус для управления опциями
|
||||||
open bool
|
open bool //Открыт ли список
|
||||||
dir string
|
dir string //Имя выбранной директории
|
||||||
currentDirectory string
|
currentDirectory string //Директория текущего файла
|
||||||
options []os.FileInfo
|
options []os.FileInfo //Элементы выбора файлов
|
||||||
optionsCount int
|
optionsCount int //Количество элементов к показу
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDirSelectModel(label, dir, description string) *DirSelectModel {
|
// NewDirSelectModel создает новую модель поля выбора директории
|
||||||
|
func NewDirSelectModel(label, dir, description string, validator textinput.ValidateFunc) *DirSelectModel {
|
||||||
inputModel := textinput.New()
|
inputModel := textinput.New()
|
||||||
|
inputModel.Validate = validator
|
||||||
inputModel.SetValue(dir)
|
inputModel.SetValue(dir)
|
||||||
return &DirSelectModel{
|
return &DirSelectModel{
|
||||||
input: inputModel,
|
input: inputModel,
|
||||||
|
|
@ -41,27 +45,37 @@ func (m *DirSelectModel) Init() tea.Cmd {
|
||||||
return textinput.Blink
|
return textinput.Blink
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update отслеживает действия с полями
|
||||||
func (m *DirSelectModel) Update(msg tea.Msg) (FieldModel, tea.Cmd) {
|
func (m *DirSelectModel) Update(msg tea.Msg) (FieldModel, tea.Cmd) {
|
||||||
var cmds []tea.Cmd
|
var cmds []tea.Cmd
|
||||||
|
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
|
//Отслеживает нажатия клавиш.
|
||||||
|
//Если список открыт, то отслеживаем клавиши отдельно
|
||||||
if m.open {
|
if m.open {
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "esc":
|
case "esc":
|
||||||
|
//Закрываем список
|
||||||
m.open = false
|
m.open = false
|
||||||
case "up":
|
case "up":
|
||||||
|
//Уменьшаем фокус, поднимаем его
|
||||||
m.focus--
|
m.focus--
|
||||||
if m.focus < 0 {
|
if m.focus < 0 {
|
||||||
m.focus = len(m.options) - 1
|
m.focus = len(m.options) - 1
|
||||||
}
|
}
|
||||||
case "down":
|
case "down":
|
||||||
|
//Увеличиваем фокус, опуская его
|
||||||
m.focus++
|
m.focus++
|
||||||
if m.focus == len(m.options) {
|
if m.focus == len(m.options) {
|
||||||
m.focus = 0
|
m.focus = 0
|
||||||
}
|
}
|
||||||
case "enter":
|
case "enter":
|
||||||
if len(m.options) == 0 || m.focus == len(m.options) {
|
//Выбираем директорию из списка. Если список пуст ничего не делаем.
|
||||||
|
//Получаем имя файла.
|
||||||
|
//Если имя файла ".." возвращаемся в родительскую папку.
|
||||||
|
//Если файл является директорией, то записываем его и закрываем список.
|
||||||
|
if len(m.options) == 0 || m.focus >= len(m.options) {
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
file := m.options[m.focus]
|
file := m.options[m.focus]
|
||||||
|
|
@ -88,6 +102,8 @@ func (m *DirSelectModel) Update(msg tea.Msg) (FieldModel, tea.Cmd) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
//Если список не открыт и был нажат enter, открываем его и рисуем опции списка.
|
||||||
|
//Иначе просто передаем сообщения нажатия клавиш в input.
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "enter":
|
case "enter":
|
||||||
m.open = true
|
m.open = true
|
||||||
|
|
@ -115,6 +131,7 @@ func (m *DirSelectModel) Update(msg tea.Msg) (FieldModel, tea.Cmd) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
//Все прочие виды сообщений передаем в input если список закрыт
|
||||||
if !m.open {
|
if !m.open {
|
||||||
var cmd tea.Cmd
|
var cmd tea.Cmd
|
||||||
m.input, cmd = m.input.Update(msg)
|
m.input, cmd = m.input.Update(msg)
|
||||||
|
|
@ -126,27 +143,13 @@ func (m *DirSelectModel) Update(msg tea.Msg) (FieldModel, tea.Cmd) {
|
||||||
|
|
||||||
func (m *DirSelectModel) View() string {
|
func (m *DirSelectModel) View() string {
|
||||||
res := fmt.Sprintf("%s: %s", m.label, m.input.View())
|
res := fmt.Sprintf("%s: %s", m.label, m.input.View())
|
||||||
if m.description != "" {
|
if m.input.Err != nil && !m.open {
|
||||||
|
res += fmt.Sprintf("\nОшибка валидации: %s", m.input.Err)
|
||||||
|
} else if m.description != "" {
|
||||||
res += fmt.Sprintf("\n%s", m.description)
|
res += fmt.Sprintf("\n%s", m.description)
|
||||||
}
|
}
|
||||||
if m.open && len(m.options) > 0 {
|
if m.open && len(m.options) > 0 {
|
||||||
visibleCount := m.optionsCount
|
start, end := utils.CalculateSelectOptionsIndex(m.focus, m.optionsCount, len(m.options))
|
||||||
if visibleCount > len(m.options) {
|
|
||||||
visibleCount = len(m.options)
|
|
||||||
}
|
|
||||||
half := visibleCount / 2
|
|
||||||
start := m.focus - half
|
|
||||||
if start < 0 {
|
|
||||||
start = 0
|
|
||||||
}
|
|
||||||
end := start + visibleCount
|
|
||||||
if end > len(m.options) {
|
|
||||||
end = len(m.options)
|
|
||||||
start = end - visibleCount
|
|
||||||
if start < 0 {
|
|
||||||
start = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i := start; i < end; i++ {
|
for i := start; i < end; i++ {
|
||||||
file := m.options[i]
|
file := m.options[i]
|
||||||
name := file.Name()
|
name := file.Name()
|
||||||
|
|
@ -188,21 +191,26 @@ func (m *DirSelectModel) GetValue() interface{} {
|
||||||
return m.dir
|
return m.dir
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RenderOptions заполняем список опций на основе директорий
|
||||||
func (m *DirSelectModel) RenderOptions() {
|
func (m *DirSelectModel) RenderOptions() {
|
||||||
|
//Читаем файлы к текущей директории
|
||||||
filesInDir, err := os.ReadDir(m.currentDirectory)
|
filesInDir, err := os.ReadDir(m.currentDirectory)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Errorf("Ошибка чтения директории: %v", err)
|
logrus.Errorf("Ошибка чтения директории: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
m.options = nil
|
m.options = nil
|
||||||
|
//Если это не корень файловой системы, добавляем переход на родительскую директорию
|
||||||
var allOptions []os.FileInfo
|
var allOptions []os.FileInfo
|
||||||
if m.currentDirectory != "/" && filepath.Clean(m.currentDirectory) != "/" {
|
if m.currentDirectory != "/" && filepath.Clean(m.currentDirectory) != "/" {
|
||||||
allOptions = append(allOptions, virtualDirInfo{name: ".."})
|
allOptions = append(allOptions, virtualDirInfo{name: ".."})
|
||||||
}
|
}
|
||||||
|
//Заполняем прочие опции
|
||||||
for _, entry := range filesInDir {
|
for _, entry := range filesInDir {
|
||||||
info, _ := entry.Info()
|
info, _ := entry.Info()
|
||||||
allOptions = append(allOptions, info)
|
allOptions = append(allOptions, info)
|
||||||
}
|
}
|
||||||
|
//Заполняем фильтр для опций на основе значения поля ввода
|
||||||
filter := ""
|
filter := ""
|
||||||
if m.open {
|
if m.open {
|
||||||
inputValue := strings.TrimSpace(m.input.Value())
|
inputValue := strings.TrimSpace(m.input.Value())
|
||||||
|
|
@ -213,6 +221,7 @@ func (m *DirSelectModel) RenderOptions() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//Чистим опции и заполняем их правильными значениями
|
||||||
m.options = m.options[:0]
|
m.options = m.options[:0]
|
||||||
for _, opt := range allOptions {
|
for _, opt := range allOptions {
|
||||||
name := opt.Name()
|
name := opt.Name()
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package fields
|
||||||
|
|
||||||
import tea "github.com/charmbracelet/bubbletea"
|
import tea "github.com/charmbracelet/bubbletea"
|
||||||
|
|
||||||
|
// FieldModel интерфейс работы с полями формы
|
||||||
type FieldModel interface {
|
type FieldModel interface {
|
||||||
Init() tea.Cmd //Метод инициализации поля
|
Init() tea.Cmd //Метод инициализации поля
|
||||||
Update(msg tea.Msg) (FieldModel, tea.Cmd) //Метод отслеживания событий связанных с моделью
|
Update(msg tea.Msg) (FieldModel, tea.Cmd) //Метод отслеживания событий связанных с моделью
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package fields
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bubble/setup/ui"
|
"bubble/setup/ui"
|
||||||
|
"bubble/setup/utils"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/charmbracelet/bubbles/textinput"
|
"github.com/charmbracelet/bubbles/textinput"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
|
@ -28,8 +29,9 @@ type FileSelectModel struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewFileSelectModel создаем новую модель поля выбора файла
|
// NewFileSelectModel создаем новую модель поля выбора файла
|
||||||
func NewFileSelectModel(label, file, description string, extensions []string) *FileSelectModel {
|
func NewFileSelectModel(label, file, description string, extensions []string, validator textinput.ValidateFunc) *FileSelectModel {
|
||||||
inputModel := textinput.New()
|
inputModel := textinput.New()
|
||||||
|
inputModel.Validate = validator
|
||||||
inputModel.SetValue(file)
|
inputModel.SetValue(file)
|
||||||
return &FileSelectModel{
|
return &FileSelectModel{
|
||||||
input: inputModel,
|
input: inputModel,
|
||||||
|
|
@ -78,7 +80,7 @@ func (m *FileSelectModel) Update(msg tea.Msg) (FieldModel, tea.Cmd) {
|
||||||
//Если имя файла ".." возвращаемся в родительскую папку.
|
//Если имя файла ".." возвращаемся в родительскую папку.
|
||||||
//Если файл является директорией, то переходим в эту директорию.
|
//Если файл является директорией, то переходим в эту директорию.
|
||||||
//Если у файла совпадают расширения, то записываем его и закрываем список.
|
//Если у файла совпадают расширения, то записываем его и закрываем список.
|
||||||
if len(m.options) == 0 || m.focus == len(m.options) {
|
if len(m.options) == 0 || m.focus >= len(m.options) {
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
file := m.options[m.focus]
|
file := m.options[m.focus]
|
||||||
|
|
@ -112,7 +114,7 @@ func (m *FileSelectModel) Update(msg tea.Msg) (FieldModel, tea.Cmd) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
//Если список не открыт и был нажат enter открываем его и рисуем опции списка.
|
//Если список не открыт и был нажат enter, открываем его и рисуем опции списка.
|
||||||
//Иначе просто передаем сообщение нажатия клавиш в input.
|
//Иначе просто передаем сообщение нажатия клавиш в input.
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "enter":
|
case "enter":
|
||||||
|
|
@ -142,27 +144,13 @@ func (m *FileSelectModel) Update(msg tea.Msg) (FieldModel, tea.Cmd) {
|
||||||
|
|
||||||
func (m *FileSelectModel) View() string {
|
func (m *FileSelectModel) View() string {
|
||||||
res := fmt.Sprintf("%s: %s", m.label, m.input.View())
|
res := fmt.Sprintf("%s: %s", m.label, m.input.View())
|
||||||
if m.description != "" {
|
if m.input.Err != nil && !m.open {
|
||||||
|
res += fmt.Sprintf("\nОшибка валидации: %s", m.input.Err)
|
||||||
|
} else if m.description != "" {
|
||||||
res += fmt.Sprintf("\n%s", m.description)
|
res += fmt.Sprintf("\n%s", m.description)
|
||||||
}
|
}
|
||||||
if m.open && len(m.options) > 0 {
|
if m.open && len(m.options) > 0 {
|
||||||
visibleCount := m.optionsCount //количество отображаемых
|
start, end := utils.CalculateSelectOptionsIndex(m.focus, m.optionsCount, len(m.options))
|
||||||
if visibleCount > len(m.options) { //если количество отображаемых больше, чем есть опций на самом деле
|
|
||||||
visibleCount = len(m.options) //то меняем количество видимых на количество опций
|
|
||||||
}
|
|
||||||
half := visibleCount / 2 //количество объектов сверху и снизу
|
|
||||||
start := m.focus - half //стартовая позиция
|
|
||||||
if start < 0 { //если стартовая позиция меньше 0, то меняем на 0
|
|
||||||
start = 0
|
|
||||||
}
|
|
||||||
end := start + visibleCount //последний элемент
|
|
||||||
if end > len(m.options) { //если последний элемент вне опций
|
|
||||||
end = len(m.options) //меняем на количество опций
|
|
||||||
start = end - visibleCount //перерасчет стартовой позиции
|
|
||||||
if start < 0 {
|
|
||||||
start = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i := start; i < end; i++ {
|
for i := start; i < end; i++ {
|
||||||
file := m.options[i]
|
file := m.options[i]
|
||||||
name := file.Name()
|
name := file.Name()
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,13 @@ type InputModel struct {
|
||||||
input textinput.Model
|
input textinput.Model
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FieldValidator func(value any) error
|
||||||
|
|
||||||
// NewInputModel создает новую модель поля ввода данных
|
// NewInputModel создает новую модель поля ввода данных
|
||||||
func NewInputModel(label, value, description string) *InputModel {
|
func NewInputModel(label, value, description string, validator textinput.ValidateFunc) *InputModel {
|
||||||
|
|
||||||
t := textinput.New()
|
t := textinput.New()
|
||||||
|
t.Validate = validator
|
||||||
t.SetValue(value)
|
t.SetValue(value)
|
||||||
t.Cursor.SetMode(cursor.CursorBlink)
|
t.Cursor.SetMode(cursor.CursorBlink)
|
||||||
return &InputModel{
|
return &InputModel{
|
||||||
|
|
@ -39,7 +43,9 @@ func (m *InputModel) Update(msg tea.Msg) (FieldModel, tea.Cmd) {
|
||||||
|
|
||||||
func (m *InputModel) View() string {
|
func (m *InputModel) View() string {
|
||||||
res := fmt.Sprintf("%s: %v", m.label, m.input.View())
|
res := fmt.Sprintf("%s: %v", m.label, m.input.View())
|
||||||
if m.description != "" {
|
if m.input.Err != nil {
|
||||||
|
res += fmt.Sprintf("\nОшибка валидации: %s", m.input.Err)
|
||||||
|
} else if m.description != "" {
|
||||||
res += fmt.Sprintf("\n%s", m.description)
|
res += fmt.Sprintf("\n%s", m.description)
|
||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,26 @@
|
||||||
package fields
|
package fields
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bubble/setup/utils"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/charmbracelet/bubbles/textinput"
|
"github.com/charmbracelet/bubbles/textinput"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// SelectModel структура описывающая модель поля выбора опции
|
||||||
type SelectModel struct {
|
type SelectModel struct {
|
||||||
label string
|
label string //Имя поля
|
||||||
description string
|
description string //Описание пол
|
||||||
options []string
|
options []string //Элементы выбора
|
||||||
focus int
|
focus int //Фокус для управления опциями
|
||||||
open bool
|
open bool //Открыт ли список
|
||||||
chosen string
|
chosen string //Выбранный элемент
|
||||||
|
optionsCount int //Количество элементов к показу
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewSelectModel создаем новую поля выбора опции
|
||||||
func NewSelectModel(label, value, description string, options []string) *SelectModel {
|
func NewSelectModel(label, value, description string, options []string) *SelectModel {
|
||||||
|
//Проверка, что указанный по умолчанию элемент есть в списке опции
|
||||||
found := false
|
found := false
|
||||||
for i := range options {
|
for i := range options {
|
||||||
if options[i] == value {
|
if options[i] == value {
|
||||||
|
|
@ -26,11 +31,12 @@ func NewSelectModel(label, value, description string, options []string) *SelectM
|
||||||
value = ""
|
value = ""
|
||||||
}
|
}
|
||||||
return &SelectModel{
|
return &SelectModel{
|
||||||
options: options,
|
options: options,
|
||||||
label: label,
|
label: label,
|
||||||
description: description,
|
description: description,
|
||||||
focus: len(options),
|
focus: len(options),
|
||||||
chosen: value,
|
chosen: value,
|
||||||
|
optionsCount: 10,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -42,30 +48,37 @@ func (m *SelectModel) Update(msg tea.Msg) (FieldModel, tea.Cmd) {
|
||||||
var cmd tea.Cmd
|
var cmd tea.Cmd
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
|
//Отслеживаем нажатия клавиш.
|
||||||
|
//Если список открыт, то отслеживаем клавиши отдельно
|
||||||
if m.open {
|
if m.open {
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "esc":
|
case "esc":
|
||||||
|
//Закрываем список
|
||||||
m.open = false
|
m.open = false
|
||||||
case "up":
|
case "up":
|
||||||
|
//Уменьшаем фокус, поднимаем его
|
||||||
m.focus--
|
m.focus--
|
||||||
if m.focus < 0 {
|
if m.focus < 0 {
|
||||||
m.focus = len(m.options) - 1
|
m.focus = len(m.options) - 1
|
||||||
}
|
}
|
||||||
case "down":
|
case "down":
|
||||||
|
//Увеличиваем фокус, опускаем его
|
||||||
m.focus++
|
m.focus++
|
||||||
if m.focus == len(m.options) {
|
if m.focus == len(m.options) {
|
||||||
m.focus = 0
|
m.focus = 0
|
||||||
}
|
}
|
||||||
case "enter":
|
case "enter":
|
||||||
if len(m.options) == 0 || m.focus == len(m.options) {
|
//Выбираем опцию из списка
|
||||||
|
if len(m.options) == 0 || m.focus >= len(m.options) {
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
m.chosen = m.options[m.focus]
|
m.chosen = m.options[m.focus]
|
||||||
m.open = false
|
m.open = false
|
||||||
m.focus = len(m.options)
|
m.focus = 0
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
//Если список не открыт и был нажат enter, открываем его и устанавливаем фокус на основе опций.
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "enter":
|
case "enter":
|
||||||
m.open = true
|
m.open = true
|
||||||
|
|
@ -91,12 +104,16 @@ func (m *SelectModel) View() string {
|
||||||
res += fmt.Sprintf("\n%s", m.description)
|
res += fmt.Sprintf("\n%s", m.description)
|
||||||
}
|
}
|
||||||
if m.open && len(m.options) > 0 {
|
if m.open && len(m.options) > 0 {
|
||||||
for i := range m.options {
|
start, end := utils.CalculateSelectOptionsIndex(m.focus, m.optionsCount, len(m.options))
|
||||||
|
for i := start; i < end; i++ {
|
||||||
|
option := m.options[i]
|
||||||
|
prefix := "\n\t"
|
||||||
if m.focus == i {
|
if m.focus == i {
|
||||||
res += "\n\t-> " + m.options[i]
|
prefix += "-> "
|
||||||
} else {
|
} else {
|
||||||
res += "\n\t " + m.options[i]
|
prefix += " "
|
||||||
}
|
}
|
||||||
|
res += prefix + option
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
|
|
|
||||||
196
setup/form.go
196
setup/form.go
|
|
@ -55,7 +55,8 @@ func NewFormModel(application *types.PackageApplication, sectionIdx, focus, widt
|
||||||
} else {
|
} else {
|
||||||
checked = sectionField.Variable.Value == "true"
|
checked = sectionField.Variable.Value == "true"
|
||||||
}
|
}
|
||||||
fieldModels = append(fieldModels, fields.NewCheckboxModel(sectionField.Name, sectionField.Description, checked))
|
validationFunc := ValidateField(sectionField.Name, sectionField.VariableName, sectionField.Validate, application)
|
||||||
|
fieldModels = append(fieldModels, fields.NewCheckboxModel(sectionField.Name, sectionField.Description, checked, validationFunc))
|
||||||
case "compute":
|
case "compute":
|
||||||
value, err := ComputeValue(sectionField.Compute, application)
|
value, err := ComputeValue(sectionField.Compute, application)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -64,11 +65,14 @@ func NewFormModel(application *types.PackageApplication, sectionIdx, focus, widt
|
||||||
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":
|
||||||
fieldModels = append(fieldModels, fields.NewFileSelectModel(sectionField.Name, fmt.Sprintf("%v", sectionField.Variable.Value), sectionField.Description, sectionField.FileFieldExtensions))
|
validationFunc := 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))
|
||||||
case "directory":
|
case "directory":
|
||||||
fieldModels = append(fieldModels, fields.NewDirSelectModel(sectionField.Name, fmt.Sprintf("%v", sectionField.Variable.Value), sectionField.Description))
|
validationFunc := ValidateField(sectionField.Name, sectionField.VariableName, sectionField.Validate, application)
|
||||||
|
fieldModels = append(fieldModels, fields.NewDirSelectModel(sectionField.Name, fmt.Sprintf("%v", sectionField.Variable.Value), sectionField.Description, validationFunc))
|
||||||
default:
|
default:
|
||||||
fieldModels = append(fieldModels, fields.NewInputModel(sectionField.Name, fmt.Sprintf("%v", sectionField.Variable.Value), sectionField.Description))
|
validationFunc := ValidateField(sectionField.Name, sectionField.VariableName, sectionField.Validate, application)
|
||||||
|
fieldModels = append(fieldModels, fields.NewInputModel(sectionField.Name, fmt.Sprintf("%v", sectionField.Variable.Value), sectionField.Description, validationFunc))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//Устанавливаем фокус (фокус может быть не равен нулю, если новая форма сформирована после Sugar события, который обновляет форму в ТЕКУЩЕМ состоянии)
|
//Устанавливаем фокус (фокус может быть не равен нулю, если новая форма сформирована после Sugar события, который обновляет форму в ТЕКУЩЕМ состоянии)
|
||||||
|
|
@ -100,7 +104,6 @@ func (m *FormModel) Init() tea.Cmd {
|
||||||
// Update отслеживает действия с формой
|
// Update отслеживает действия с формой
|
||||||
func (m *FormModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (m *FormModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
|
||||||
//todo сообщение для переноса viewport вверх или внизе
|
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case tea.WindowSizeMsg:
|
case tea.WindowSizeMsg:
|
||||||
//Отслеживаем и записываем новые размеры окна, обновляем рендер about
|
//Отслеживаем и записываем новые размеры окна, обновляем рендер about
|
||||||
|
|
@ -189,15 +192,31 @@ func (m *FormModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
}
|
}
|
||||||
//Проверка правильности фокуса, чтобы не выйти за границы массива полей формы, необходимо проверить новое значение фокуса.
|
//Проверка правильности фокуса, чтобы не выйти за границы массива полей формы, необходимо проверить новое значение фокуса.
|
||||||
//Если фокус стал больше чем количество полей формы, значит он вышел за пределы всей формы и находится уже даже ниже кнопки,
|
//Если фокус стал больше чем количество полей формы, значит он вышел за пределы всей формы и находится уже даже ниже кнопки,
|
||||||
//переводим его на первую позицию, то есть на 0.
|
//переводим его на первую позицию, то есть на 0 и перемещаем видимую область наверх.
|
||||||
//Если фокус стал меньше 0, значит он вышел за пределы формы и находится выше первого поля формы,
|
//Если фокус стал меньше 0, значит он вышел за пределы формы и находится выше первого поля формы,
|
||||||
//переводим его на последнюю позицию, то есть на кнопку переключения секции, равную положение которой = количество полей на форме.
|
//переводим его на последнюю позицию, то есть на кнопку переключения секции, равную положение которой = количество полей на форме и перемещаем видимую область вниз.
|
||||||
if m.focus == len(m.FieldModels)+1 {
|
if m.focus == len(m.FieldModels)+1 {
|
||||||
m.focus = 0
|
m.focus = 0
|
||||||
|
return m, func() tea.Msg {
|
||||||
|
return types.ViewportScrollMsg{
|
||||||
|
Direction: "up",
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if m.focus < 0 {
|
} else if m.focus < 0 {
|
||||||
m.focus = len(m.FieldModels)
|
m.focus = len(m.FieldModels)
|
||||||
|
return m, func() tea.Msg {
|
||||||
|
return types.ViewportScrollMsg{
|
||||||
|
Direction: "down",
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case types.ViewportScrollMsg:
|
||||||
|
if msg.Direction == "up" {
|
||||||
|
m.viewport.GotoTop()
|
||||||
|
} else {
|
||||||
|
m.viewport.GotoBottom()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
var viewportCmd, fieldsCmd tea.Cmd
|
var viewportCmd, fieldsCmd tea.Cmd
|
||||||
m.viewport, viewportCmd = m.viewport.Update(msg)
|
m.viewport, viewportCmd = m.viewport.Update(msg)
|
||||||
|
|
@ -225,11 +244,9 @@ func (m *FormModel) View() string {
|
||||||
|
|
||||||
func (m *FormModel) initViewport(width, height int) {
|
func (m *FormModel) initViewport(width, height int) {
|
||||||
_, _, contentWidth, contentHeight := ui.ContentSizes(width, height)
|
_, _, contentWidth, contentHeight := ui.ContentSizes(width, height)
|
||||||
contentAreaWidth := contentWidth - 2*1
|
m.viewport = viewport.New(contentWidth, contentHeight)
|
||||||
contentAreaHeight := contentHeight - 2*1
|
|
||||||
m.viewport = viewport.New(contentWidth, contentAreaHeight)
|
|
||||||
m.viewport.SetHorizontalStep(1)
|
m.viewport.SetHorizontalStep(1)
|
||||||
m.rebuildContent(contentAreaWidth)
|
m.rebuildContent(contentWidth)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *FormModel) rebuildContent(width int) {
|
func (m *FormModel) rebuildContent(width int) {
|
||||||
|
|
@ -257,11 +274,18 @@ func simpleForm(fields []fields.FieldModel, focus, width int) string {
|
||||||
//Имеется ввиду, что если вводимое значение слишком длинное, то оно будет делиться на несколько строк.
|
//Имеется ввиду, что если вводимое значение слишком длинное, то оно будет делиться на несколько строк.
|
||||||
fieldStroke := wordwrap.String(field.View(), width)
|
fieldStroke := wordwrap.String(field.View(), width)
|
||||||
//Если фокус равен текущему полю, то выделяем его.
|
//Если фокус равен текущему полю, то выделяем его.
|
||||||
|
var finalStroke string
|
||||||
if i == focus {
|
if i == focus {
|
||||||
sb.WriteString(ui.FormFieldFocused.Render(fieldStroke) + "\n\n")
|
finalStroke = ui.FormFieldFocused.Render(fieldStroke)
|
||||||
} else {
|
} else {
|
||||||
sb.WriteString(ui.FormFieldUnfocused.Render(fieldStroke) + "\n\n")
|
finalStroke = ui.FormFieldUnfocused.Render(fieldStroke)
|
||||||
}
|
}
|
||||||
|
if i != len(fields)-1 {
|
||||||
|
finalStroke += "\n\n"
|
||||||
|
} else {
|
||||||
|
finalStroke += "\n"
|
||||||
|
}
|
||||||
|
sb.WriteString(finalStroke)
|
||||||
}
|
}
|
||||||
return sb.String()
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|
@ -269,12 +293,13 @@ func simpleForm(fields []fields.FieldModel, focus, width int) string {
|
||||||
// simpleAboutRender формирует единую строку, описывающую настраиваемое приложение.
|
// simpleAboutRender формирует единую строку, описывающую настраиваемое приложение.
|
||||||
func simpleAboutRender(paragraphs []string, width int) string {
|
func simpleAboutRender(paragraphs []string, width int) string {
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
for _, p := range paragraphs {
|
for i, p := range paragraphs {
|
||||||
p = strings.TrimSpace(p)
|
p = strings.TrimSpace(p)
|
||||||
if p == "" {
|
if p == "" {
|
||||||
sb.WriteString("\n")
|
sb.WriteString("\n")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
var finalStroke, strokeType string
|
||||||
switch {
|
switch {
|
||||||
case strings.HasPrefix(p, "# "):
|
case strings.HasPrefix(p, "# "):
|
||||||
// Заголовок 1 уровня
|
// Заголовок 1 уровня
|
||||||
|
|
@ -283,32 +308,49 @@ func simpleAboutRender(paragraphs []string, width int) string {
|
||||||
//sb.WriteString(ui.AboutTitle.Render(h1Offset+p[2:]) + "\n\n")
|
//sb.WriteString(ui.AboutTitle.Render(h1Offset+p[2:]) + "\n\n")
|
||||||
text := strings.TrimPrefix(p, "# ")
|
text := strings.TrimPrefix(p, "# ")
|
||||||
wrapped := wordwrap.String(text, width)
|
wrapped := wordwrap.String(text, width)
|
||||||
sb.WriteString(ui.AboutTitle.Render(wrapped) + "\n\n")
|
finalStroke = ui.AboutTitle.Render(wrapped)
|
||||||
|
strokeType = "header1"
|
||||||
case strings.HasPrefix(p, "## "):
|
case strings.HasPrefix(p, "## "):
|
||||||
// Заголовок 2 уровня
|
// Заголовок 2 уровня
|
||||||
text := strings.TrimPrefix(p, "## ")
|
text := strings.TrimPrefix(p, "## ")
|
||||||
wrapped := wordwrap.String(text, width)
|
wrapped := wordwrap.String(text, width)
|
||||||
sb.WriteString(ui.AboutH2.Render(wrapped) + "\n\n")
|
finalStroke = ui.AboutH2.Render(wrapped)
|
||||||
|
strokeType = "header2"
|
||||||
case strings.HasPrefix(p, "### "):
|
case strings.HasPrefix(p, "### "):
|
||||||
// Заголовок 3 уровня
|
// Заголовок 3 уровня
|
||||||
text := strings.TrimPrefix(p, "### ")
|
text := strings.TrimPrefix(p, "### ")
|
||||||
wrapped := wordwrap.String(text, width)
|
wrapped := wordwrap.String(text, width)
|
||||||
sb.WriteString(ui.AboutH3.Render(wrapped) + "\n\n")
|
finalStroke = ui.AboutH3.Render(wrapped)
|
||||||
|
strokeType = "header3"
|
||||||
case strings.HasPrefix(p, "- "), strings.HasPrefix(p, "* "):
|
case strings.HasPrefix(p, "- "), strings.HasPrefix(p, "* "):
|
||||||
// Список простой
|
// Список простой
|
||||||
text := strings.TrimLeft(p[2:], " ")
|
text := strings.TrimLeft(p[2:], " ")
|
||||||
wrapped := wordwrap.String(text, width)
|
wrapped := wordwrap.String(text, width)
|
||||||
sb.WriteString(" " + ui.AboutBullet.Render("• ") + ui.AboutNormal.Render(wrapped) + "\n")
|
finalStroke = " " + ui.AboutBullet.Render("• ") + ui.AboutNormal.Render(wrapped)
|
||||||
|
strokeType = "bullet"
|
||||||
case strings.HasPrefix(p, "> "):
|
case strings.HasPrefix(p, "> "):
|
||||||
// Цитаты
|
// Цитаты
|
||||||
text := strings.TrimPrefix(p, "> ")
|
text := strings.TrimPrefix(p, "> ")
|
||||||
wrapped := wordwrap.String(text, width)
|
wrapped := wordwrap.String(text, width)
|
||||||
sb.WriteString(" " + ui.AboutNormal.Foreground(ui.ColorGrey).Render("│ "+wrapped) + "\n")
|
finalStroke = " " + ui.AboutNormal.Foreground(ui.ColorGrey).Render("│ "+wrapped)
|
||||||
|
strokeType = "quote"
|
||||||
default:
|
default:
|
||||||
// Обычный текст
|
// Обычный текст
|
||||||
wrapped := wordwrap.String(p, width)
|
wrapped := wordwrap.String(p, width)
|
||||||
sb.WriteString(" " + ui.AboutNormal.Render(wrapped) + "\n\n")
|
finalStroke = " " + ui.AboutNormal.Render(wrapped)
|
||||||
|
strokeType = "normal"
|
||||||
}
|
}
|
||||||
|
if i != len(paragraphs)-1 {
|
||||||
|
switch strokeType {
|
||||||
|
case "normal", "quote", "bullet":
|
||||||
|
finalStroke += "\n"
|
||||||
|
case "header1", "header2", "header3":
|
||||||
|
finalStroke += "\n\n"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
finalStroke += "\n"
|
||||||
|
}
|
||||||
|
sb.WriteString(finalStroke)
|
||||||
}
|
}
|
||||||
return sb.String()
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|
@ -350,7 +392,6 @@ func (m *FormModel) updateFields(msg tea.Msg) tea.Cmd {
|
||||||
return types.SugarMsg{
|
return types.SugarMsg{
|
||||||
VariableName: sectionField.VariableName,
|
VariableName: sectionField.VariableName,
|
||||||
FieldName: sectionField.Name,
|
FieldName: sectionField.Name,
|
||||||
Compute: sectionField.Compute,
|
|
||||||
Focus: m.focus,
|
Focus: m.focus,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -367,116 +408,3 @@ func (m *FormModel) updateFields(msg tea.Msg) tea.Cmd {
|
||||||
//Объединяем весь массив команду в одну
|
//Объединяем весь массив команду в одну
|
||||||
return tea.Batch(cmds...)
|
return tea.Batch(cmds...)
|
||||||
}
|
}
|
||||||
|
|
||||||
//// Update отслеживает действия с формой
|
|
||||||
//func (m *FormModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
||||||
//
|
|
||||||
// switch msg := msg.(type) {
|
|
||||||
// case tea.WindowSizeMsg:
|
|
||||||
// //Отслеживаем и записываем новые размеры окна, обновляем рендер about
|
|
||||||
// m.width = msg.Width
|
|
||||||
// m.height = msg.Height
|
|
||||||
// m.initViewport(m.width, m.height)
|
|
||||||
// //if m.CurrentSection.Type == types.SectionAbout {
|
|
||||||
// // m.renderedContent = simpleAboutRender(m.CurrentSection.Paragraphs, m.width)
|
|
||||||
// //}
|
|
||||||
// return m, nil
|
|
||||||
// case tea.KeyMsg:
|
|
||||||
// //Отслеживаем нажатие клавиш формы
|
|
||||||
// switch msg.String() {
|
|
||||||
// case "esc":
|
|
||||||
// //Обработчик клавиши escape на форме нужен для закрытия полей поддерживающих открытие.
|
|
||||||
// //Обходим все доступные в текущей форме поля и закрываем те, что были открыты.
|
|
||||||
// //Проверять на фокус не нужно, так как не предполагается, что у нас может быть открыто сразу несколько полей.
|
|
||||||
// //Поэтому просто возвращаем команду по возврату сообщение об обновлении полей.
|
|
||||||
// for _, field := range m.FieldModels {
|
|
||||||
// //проверяем открыто ли поле
|
|
||||||
// if field.CaptureKeys() {
|
|
||||||
// //обновляем поля если оно открыто
|
|
||||||
// return m, m.updateFields(msg)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// //Если открытых полей не было найдено, значит escape была нажата для закрытия настройки,
|
|
||||||
// //возвращаем команду по возврату сообщения об открытии модального окна
|
|
||||||
// return m, func() tea.Msg {
|
|
||||||
// return types.ShowModalMsg{
|
|
||||||
// Type: types.ModalWarning,
|
|
||||||
// Title: "Прервать настройку",
|
|
||||||
// Message: "Вы уверены, что хотите прервать настройку?\nВсе введенные данные будут потеряны.",
|
|
||||||
// ModalButtons: []types.ModalButton{
|
|
||||||
// {Text: "Да, выйти", Value: true},
|
|
||||||
// {Text: "Продолжить настройку", Value: false},
|
|
||||||
// },
|
|
||||||
// OnResult: func(result any) tea.Cmd {
|
|
||||||
// if result == true {
|
|
||||||
// return func() tea.Msg { return types.BackToListMsg{} }
|
|
||||||
// }
|
|
||||||
// return nil
|
|
||||||
// },
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// case "enter":
|
|
||||||
// //Обработчик клавиши enter на форме нужен для перехода на следующую секцию.
|
|
||||||
// //Проверяем где сейчас находится фокус, если его позиция равняется количеству полей,
|
|
||||||
// //значит он вышел за пределы формы и находится на кнопке next section, что ставится последней.
|
|
||||||
// //После этого мы сбрасываем фокус и возвращаем команду по возврату сообщения о переходе на следующую секцию
|
|
||||||
// if len(m.FieldModels) == m.focus {
|
|
||||||
// m.focus = 0
|
|
||||||
// return m, func() tea.Msg {
|
|
||||||
// return types.NextSectionMsg{}
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// case "tab", "shift+tab", "down", "up":
|
|
||||||
// //Обработчик клавиш tab, shift+tab, down, up на форме нужен для перехода между объектами формы.
|
|
||||||
// //Получаем точное имя клавиши нажатия, после проверяем где сейчас находится фокус.
|
|
||||||
// //Если фокус меньше количества полей, то значит фокус сейчас на поле формы.
|
|
||||||
// // Подробнее. У нас фокус в двух состояниях так сказать,
|
|
||||||
// // фокус равен количество полей, значит он на кнопке следующей секции,
|
|
||||||
// // если меньше количества полей или равен 0, значит фокус на поле формы.
|
|
||||||
// // Если у нас нет полей на форме (секция About), значит количество полей равно 0, а фокус по умолчанию устанавливается 0.
|
|
||||||
// // Чтобы m.FieldModels[m.focus] вызвало ошибку "индекс вне массива",
|
|
||||||
// // нужно чтобы в условие m.focus < len(m.FieldModels) попал фокус меньше 0 (-1,-2 и тд).
|
|
||||||
// // Чтобы этого избежать, есть проверка, которая устанавливает m.focus на последнюю позицию (количество полей на форме).
|
|
||||||
// // Чтобы эта проверка дала сбой, нужно после неё прописать изменение фокуса формы на отрицательный принудительно, так как такое не может произойти в текущем коде.
|
|
||||||
// //Получаем имя поля (так как это старый фокус, то и поле на котором мы находимся это предыдущее поле, отсюда и название переменной),
|
|
||||||
// //Если поле не открыто, то мы можем перейти на следующее поле, увеличив или уменьшив фокус.
|
|
||||||
// //Если фокус равен количеству полей, значит он находится на кнопке переключения секций. Увеличиваем или уменьшаем фокус.
|
|
||||||
// key := msg.String()
|
|
||||||
// if m.focus < len(m.FieldModels) {
|
|
||||||
// prevField := m.FieldModels[m.focus]
|
|
||||||
// if !prevField.CaptureKeys() {
|
|
||||||
// if key == "tab" || key == "down" {
|
|
||||||
// m.focus++
|
|
||||||
// } else if key == "shift+tab" || key == "up" {
|
|
||||||
// m.focus--
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// } else {
|
|
||||||
// if key == "tab" || key == "down" {
|
|
||||||
// m.focus++
|
|
||||||
// } else if key == "shift+tab" || key == "up" {
|
|
||||||
// m.focus--
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// //Проверка правильности фокуса, чтобы не выйти за границы массива полей формы, необходимо проверить новое значение фокуса.
|
|
||||||
// //Если фокус стал больше чем количество полей формы, значит он вышел за пределы всей формы и находится уже даже ниже кнопки,
|
|
||||||
// //переводим его на первую позицию, то есть на 0.
|
|
||||||
// //Если фокус стал меньше 0, значит он вышел за пределы формы и находится выше первого поля формы,
|
|
||||||
// //переводим его на последнюю позицию, то есть на кнопку переключения секции, равную положение которой = количество полей на форме.
|
|
||||||
// if m.focus == len(m.FieldModels)+1 {
|
|
||||||
// m.focus = 0
|
|
||||||
// } else if m.focus < 0 {
|
|
||||||
// m.focus = len(m.FieldModels)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// //Если не было возвращено сообщений, значит действие было произведено с полем, а не с самой формой.
|
|
||||||
// //Возвращаем результат функции обновления полей.
|
|
||||||
// var cmd tea.Cmd
|
|
||||||
// if m.CurrentSection.Type == types.SectionAbout {
|
|
||||||
// m.viewport, cmd = m.viewport.Update(msg)
|
|
||||||
// return m, cmd
|
|
||||||
// } else {
|
|
||||||
// return m, m.updateFields(msg)
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
|
|
|
||||||
|
|
@ -213,7 +213,7 @@ func (m *Main) Sugar(sugar types.SugarMsg) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsParent проверяет являться ли указанная переменная условие для поля или секции
|
// IsParent проверяет, является ли указанная переменная условием для поля или секции
|
||||||
func IsParent(variableName string, sections []types.Section) bool {
|
func IsParent(variableName string, sections []types.Section) bool {
|
||||||
for _, section := range sections {
|
for _, section := range sections {
|
||||||
for _, parent := range section.Parent {
|
for _, parent := range section.Parent {
|
||||||
|
|
|
||||||
31
setup/utils/utils.go
Normal file
31
setup/utils/utils.go
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
// CalculateSelectOptionsIndex рассчитывает индекс первой и последней опции для отображения в поле выбора
|
||||||
|
func CalculateSelectOptionsIndex(focus, visibleCount, optionsCount int) (int, int) {
|
||||||
|
//Если количество отображаемых больше, чем есть количество опций на самом деле,
|
||||||
|
//то меняем количество опций для отображения на количество опций.
|
||||||
|
if visibleCount > optionsCount {
|
||||||
|
visibleCount = optionsCount
|
||||||
|
}
|
||||||
|
//Количество объектов сверху и снизу
|
||||||
|
half := visibleCount / 2
|
||||||
|
//Индекс первой опции для показа в списке
|
||||||
|
start := focus - half
|
||||||
|
//Если индекс меньше 0, то фиксируем его на первой
|
||||||
|
if start < 0 {
|
||||||
|
start = 0
|
||||||
|
}
|
||||||
|
//Индекс последней опции для показа в списке
|
||||||
|
end := start + visibleCount
|
||||||
|
//Если индекс больше, чем количество опций для вывода,
|
||||||
|
//то фиксируем его на последней опции.
|
||||||
|
//Производим перерасчет индекса первой опции
|
||||||
|
if end > optionsCount {
|
||||||
|
end = optionsCount
|
||||||
|
start = end - visibleCount
|
||||||
|
if start < 0 {
|
||||||
|
start = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return start, end
|
||||||
|
}
|
||||||
35
types/msg.go
35
types/msg.go
|
|
@ -2,32 +2,43 @@ package types
|
||||||
|
|
||||||
import tea "github.com/charmbracelet/bubbletea"
|
import tea "github.com/charmbracelet/bubbletea"
|
||||||
|
|
||||||
|
// SelectedAppMsg сообщение выбора приложения из списка приложений
|
||||||
type SelectedAppMsg struct {
|
type SelectedAppMsg struct {
|
||||||
App PackageApplication
|
App PackageApplication //Объект выбранного приложения
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BackToListMsg сообщение перехода к списку приложений
|
||||||
type BackToListMsg struct{}
|
type BackToListMsg struct{}
|
||||||
|
|
||||||
|
// NextSectionMsg сообщение перехода на следующую секцию
|
||||||
type NextSectionMsg struct{}
|
type NextSectionMsg struct{}
|
||||||
|
|
||||||
|
// SugarMsg сообщение о возможной необходимости обновить форму
|
||||||
type SugarMsg struct {
|
type SugarMsg struct {
|
||||||
VariableName string
|
VariableName string //Имя переменной присвоенной измененному полю
|
||||||
FieldName string
|
FieldName string //Имя измененного поля
|
||||||
Compute string
|
Focus int //Старый фокус формы
|
||||||
Focus int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ShowModalMsg сообщение о показе модального окна
|
||||||
type ShowModalMsg struct {
|
type ShowModalMsg struct {
|
||||||
Type ModalType
|
Type ModalType //Тип модального окна
|
||||||
Title string
|
Title string //Заголовок модального окна
|
||||||
Message string
|
Message string //Сообщение модального окна
|
||||||
ModalButtons []ModalButton
|
ModalButtons []ModalButton //Кнопки модального окна
|
||||||
OnResult func(result any) tea.Cmd
|
OnResult func(result any) tea.Cmd //Функция для выполнения
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ModalButton структура кнопки модального окна
|
||||||
type ModalButton struct {
|
type ModalButton struct {
|
||||||
Text string
|
Text string //Текст кнопки
|
||||||
Value any
|
Value any //Значение кнопки
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CloseModalMsg сообщение о закрытии модального окна
|
||||||
type CloseModalMsg struct{}
|
type CloseModalMsg struct{}
|
||||||
|
|
||||||
|
// ViewportScrollMsg сообщение прокрутки видимой области формы
|
||||||
|
type ViewportScrollMsg struct {
|
||||||
|
Direction string //Направление прокрутки
|
||||||
|
}
|
||||||
|
|
|
||||||
103
types/types.go
103
types/types.go
|
|
@ -1,70 +1,77 @@
|
||||||
package types
|
package types
|
||||||
|
|
||||||
|
// PackageApplication структура описывающая настраиваемое приложение
|
||||||
type PackageApplication struct {
|
type PackageApplication struct {
|
||||||
Name string `yaml:"name"`
|
Name string `yaml:"name"` //Имя приложения
|
||||||
Version string `yaml:"version"`
|
Version string `yaml:"version"` //Версия приложения
|
||||||
Description string `yaml:"description"`
|
Description string `yaml:"description"` //Описание приложения
|
||||||
Icon string `yaml:"icon"`
|
Icon string `yaml:"icon"` //Иконка приложения
|
||||||
Type string `yaml:"type"`
|
Type string `yaml:"type"` //Тип приложения
|
||||||
ConfFiles []ConfFile `yaml:"conf-files"`
|
ConfFiles []ConfFile `yaml:"conf-files"` //Файлы конфигурации
|
||||||
Variables []Variable `yaml:"variables"`
|
Variables []Variable `yaml:"variables"` //Переменные настройки
|
||||||
Sections []Section `yaml:"sections"`
|
Sections []Section `yaml:"sections"` //Секции настройки
|
||||||
Changed bool `yaml:"-"`
|
Changed bool `yaml:"-"` //Вносились ли изменения
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ConfFile структура описывающая используемый приложением конфигурационный файл
|
||||||
type ConfFile struct {
|
type ConfFile struct {
|
||||||
File string `yaml:"file"`
|
File string `yaml:"file"` //Путь файла
|
||||||
Type string `yaml:"type"`
|
Type string `yaml:"type"` //Тип файла
|
||||||
Name string `yaml:"name"`
|
Name string `yaml:"name"` //Имя файла
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Variable структура описывающая переменную для настройки приложения
|
||||||
type Variable struct {
|
type Variable struct {
|
||||||
Name string `yaml:"name"`
|
Name string `yaml:"name"` //Имя переменной
|
||||||
Type string `yaml:"type"`
|
Type string `yaml:"type"` //Тип значения переменной
|
||||||
Default string `yaml:"default"`
|
Default string `yaml:"default"` //Значение по умолчанию
|
||||||
Value interface{} `yaml:"-"`
|
Value interface{} `yaml:"-"` //Используемое значение
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Section структура описывающая секцию настройки приложения
|
||||||
type Section struct {
|
type Section struct {
|
||||||
Name string `yaml:"name"`
|
Name string `yaml:"name"` //Имя секции
|
||||||
Type string `yaml:"type"`
|
Type string `yaml:"type"` //Тип секции
|
||||||
Description string `yaml:"description"`
|
Description string `yaml:"description"` //Описание секции
|
||||||
Fields []SectionField `yaml:"fields"`
|
Fields []SectionField `yaml:"fields"` //Поля секции
|
||||||
Paragraphs []string `yaml:"paragraphs"`
|
Paragraphs []string `yaml:"paragraphs"` //Параграфы для описания приложения
|
||||||
Parent []string `yaml:"parent"`
|
Parent []string `yaml:"parent"` //Переменные от которых зависит секция
|
||||||
Condition string `yaml:"condition"`
|
Condition string `yaml:"condition"` //Условие отображения секции
|
||||||
Actions []SectionAction `yaml:"actions"`
|
Actions []SectionAction `yaml:"actions"` //Действия выполняемые после перехода с секции
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SectionField структура описывающая поле секции
|
||||||
type SectionField struct {
|
type SectionField struct {
|
||||||
Name string `yaml:"name"`
|
Name string `yaml:"name"` //Имя поля
|
||||||
VariableName string `yaml:"variable-name"`
|
VariableName string `yaml:"variable-name"` //Имя переменной, хранящей значение поля
|
||||||
FieldType string `yaml:"field-type"`
|
FieldType string `yaml:"field-type"` //Тип поля
|
||||||
FileFieldExtensions []string `yaml:"file-field-extensions"`
|
FileFieldExtensions []string `yaml:"file-field-extensions"` //Расширения файла для поля выбора файла
|
||||||
Description string `yaml:"description"`
|
Description string `yaml:"description"` //Описание поля
|
||||||
Condition string `yaml:"condition"`
|
Condition string `yaml:"condition"` //Условие отображения поля
|
||||||
Options []interface{} `yaml:"options"`
|
Options []interface{} `yaml:"options"` //Список опций
|
||||||
SpecialValue string `yaml:"special-value"`
|
SpecialValue string `yaml:"special-value"` //Специального значения
|
||||||
Compute string `yaml:"compute"`
|
Compute string `yaml:"compute"` //Строка вычислений значения
|
||||||
Validate []FieldValidate `yaml:"validate"`
|
Validate []FieldValidate `yaml:"validate"` //Правила проверки значения
|
||||||
Parent []string `yaml:"parent"`
|
Parent []string `yaml:"parent"` //Переменные от которых зависит поле
|
||||||
Variable *Variable `yaml:"-"`
|
Variable *Variable `yaml:"-"` //Указатель на переменную
|
||||||
Active bool `yaml:"-"`
|
Active bool `yaml:"-"` //Активно ли поле для получения значения
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FieldValidate структура описывающая правила проверки значения поля
|
||||||
type FieldValidate struct {
|
type FieldValidate struct {
|
||||||
Rule string `yaml:"rule"`
|
Rule string `yaml:"rule"` //Правило проверки
|
||||||
Message string `yaml:"message"`
|
Message string `yaml:"message"` //Сообщение ошибки
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SectionAction структура описывающая действия к выполнению
|
||||||
type SectionAction struct {
|
type SectionAction struct {
|
||||||
Name string `yaml:"name"`
|
Name string `yaml:"name"` //Имя действия
|
||||||
Type string `yaml:"type"`
|
Type string `yaml:"type"` //Тип действия
|
||||||
Command string `yaml:"command"`
|
Command string `yaml:"command"` //Команда для выполнения
|
||||||
FileTarget string `yaml:"file-target"`
|
FileTarget string `yaml:"file-target"` //Целевой файл редактирования
|
||||||
FileSource string `yaml:"file-source"`
|
FileSource string `yaml:"file-source"` //Файл источник
|
||||||
Condition string `yaml:"condition"`
|
Condition string `yaml:"condition"` //Условие для выполнения
|
||||||
RunAs string `yaml:"run-as"`
|
RunAs string `yaml:"run-as"` //Пользователь для запуска
|
||||||
RequirePackages []string `yaml:"require-packages"`
|
RequirePackages []string `yaml:"require-packages"` //Требуемые пакеты
|
||||||
Error string `yaml:"error"`
|
Error string `yaml:"error"` //Ошибка выполнения
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue