GoSetup-bubble/setup/form.go

334 lines
17 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package setup
import (
"bubble/setup/fields"
"bubble/setup/ui"
"bubble/types"
"fmt"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/muesli/reflow/wordwrap"
"github.com/sirupsen/logrus"
"strings"
)
// FormModel структура описывающая модель отрисовки формы настройки
type FormModel struct {
CurrentSection *types.Section //Указатель секции с которой надо работать
FieldModels []fields.FieldModel //Массив моделей полей
focus int //Фокус для отслеживания с каким объектом надо работать
renderedContent string //Строка хранения информации о приложении
width int //Ширина окна
height int //Высота окна
}
// NewFormModel создает новую модель формы на основе данных настраиваемого приложения
func NewFormModel(application *types.PackageApplication, sectionIdx, focus, width, height int) *FormModel {
//Получаем секцию которую надо отрисовать
currentSection := &application.Sections[sectionIdx]
//Заполняем массив моделей полей с учетом условий
var fieldModels []fields.FieldModel
for _, sectionField := range currentSection.Fields {
if !CheckCondition(sectionField.Condition, application) {
continue
}
switch sectionField.FieldType {
case "select":
var options []string
for j := range sectionField.Options {
options = append(options, fmt.Sprintf("%v", sectionField.Options[j]))
}
if sectionField.SpecialValue != "" {
specialOptions := SpecialValueHandle(sectionField.SpecialValue)
if strSpecial, ok := specialOptions.([]string); ok {
options = strSpecial
}
}
selected := fmt.Sprintf("%v", sectionField.Variable.Value)
fieldModels = append(fieldModels, fields.NewSelectModel(sectionField.Name, selected, options))
case "check":
checked := false
if value, ok := sectionField.Variable.Value.(bool); ok {
checked = value
} else {
checked = sectionField.Variable.Value == "true"
}
fieldModels = append(fieldModels, fields.NewCheckboxModel(sectionField.Name, checked))
case "compute":
value, err := ComputeValue(sectionField.Compute, application)
if err != nil {
value = fmt.Sprintf("Ошибка вычисления: %v", err)
}
fieldModels = append(fieldModels, fields.NewComputeModel(sectionField.Name, fmt.Sprintf("%v", value)))
//compute расчет
case "file":
fieldModels = append(fieldModels, fields.NewFileSelectModel(sectionField.Name, fmt.Sprintf("%v", sectionField.Variable.Value), sectionField.FileFieldExtensions))
case "directory":
fieldModels = append(fieldModels, fields.NewDirSelectModel(sectionField.Name, fmt.Sprintf("%v", sectionField.Variable.Value)))
default:
fieldModels = append(fieldModels, fields.NewInputModel(sectionField.Name, fmt.Sprintf("%v", sectionField.Variable.Value)))
}
}
//Устанавливаем фокус (фокус может быть не равен нулю, если новая форма сформирована после Sugar события, который обновляет форму в ТЕКУЩЕМ состоянии)
if len(fieldModels) > focus {
fieldModels[focus].Focus()
}
m := &FormModel{
FieldModels: fieldModels,
focus: focus,
CurrentSection: currentSection,
width: width,
height: height,
}
//Формируем описание приложения если секция установлена About
if currentSection.Type == types.SectionAbout {
m.renderedContent = simpleAboutRender(currentSection.Paragraphs, width)
m.focus = 0
}
return m
}
func (m *FormModel) Init() tea.Cmd {
return nil
}
// 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
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 на форме нужен для перехода между объектами формы.
//Получаем точное имя клавиши нажатия, после проверяем где сейчас находится фокус.
//Если фокус не равен количеству полей, то значит фокус сейчас у поля формы.
//Получаем имя поля (так как это старый фокус, то и поле на котором мы находимся это предыдущее поле, отсюда и название переменной),
//Если поле не открыто, то мы можем перейти на следующее поле, увеличив или уменьшив фокус.
//Если фокус равен количеству полей, значит он находится на кнопке переключения секций. Увеличиваем или уменьшаем фокус.
key := msg.String()
if m.focus != len(m.FieldModels) {
//todo исправить фокус
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)
}
}
case types.SelectedOptionMsg:
//Отслеживаем выбор опции в поле типа select
//todo тут вообще логика нужна? Мы ведь в select, через m.chosen записываем значение, а после GetValue метод возвращаем m.chosen
logrus.Infof("chose %+v", msg)
}
//Если не было возвращено сообщений, значит действие было произведено с полем, а не с самой формой.
//Возвращаем результат функции обновления полей.
return m, m.updateFields(msg)
}
// View формирует строку для вывода формы и её контента.
func (m *FormModel) View() string {
//Данные о размерах контейнера и ширины контента внутри
containerWidth, containerHeight, contentWidth, _ := ui.ContentSizes(m.width, m.height)
formWidth := contentWidth - 2*1
var strokes []string //отображаемые строки
for i, field := range m.FieldModels {
//Строка с учетом переноса поля.
//Имеется ввиду, что если вводимое значение слишком длинное, то оно будет делиться на несколько строк.
fieldStroke := wordwrap.String(field.View(), formWidth)
//Если фокус равен текущему полю, то выделяем его.
if i == m.focus {
strokes = append(strokes, ui.FormFieldFocused.Render(fieldStroke))
} else {
strokes = append(strokes, ui.FormFieldUnfocused.Render(fieldStroke))
}
}
//Если текущая секция это секция About, то выводим её информацию.
//Не предполагается, что информация о приложении и поля для настройки будут в одной секции, поэтому тут идет обнуление прошлых строк.
if m.CurrentSection.Type == types.SectionAbout {
strokes = append([]string{}, m.renderedContent)
}
//Строка кнопки переключения секций.
//Если позиция фокуса равняется количеству полей, значит он сейчас находится на кнопке.
nextBtn := "[ next section ]"
if m.focus == len(m.FieldModels) {
strokes = append(strokes, ui.FormNextButton.Render(nextBtn))
} else {
strokes = append(strokes, ui.FormFieldUnfocused.Render(nextBtn))
}
//Основной контейнер приложения.
//Устанавливаем ему ширину и высоту, объединяем все строки в одну с соединением, через двойной перевод строки.
container := ui.AppContainer.
Width(containerWidth).
Height(containerHeight).
Render(strings.Join(strokes, "\n\n"))
return lipgloss.Place(
m.width, m.height,
lipgloss.Center, lipgloss.Center,
container,
lipgloss.WithWhitespaceChars(" "),
)
}
// updateFields обновляет поля и их внутренние данные, формирует единую команду действий для всех полей, в том числе возврат SugarMsg
func (m *FormModel) updateFields(msg tea.Msg) tea.Cmd {
//Массив команд для выполнения
var cmds []tea.Cmd
for i, fieldModel := range m.FieldModels {
var fieldCmd tea.Cmd
var updated fields.FieldModel
//Если фокус установлен на текущем поле, то нужно активировать его и передать ему сообщение для обновления.
//Если фокус не установлен на текущем поле, то снимаем с него фокус и передаем ему сообщение не связанное с нажатием кнопок клавиатуры
if i == m.focus {
fieldModel.Focus()
updated, fieldCmd = fieldModel.Update(msg)
} else {
fieldModel.Blur()
if _, ok := msg.(tea.KeyMsg); !ok {
updated, fieldCmd = fieldModel.Update(msg)
}
}
//Если поле успешно вернуло обновленное поле, то необходимо:
//обновить модель поля в текущей форме;
//проверить изменилось ли значение переменной в настройках приложения,
//если изменилось, в массив команд помещаем команду по возврату сообщения Sugar
if updated != nil {
m.FieldModels[i] = updated
for _, sectionField := range m.CurrentSection.Fields {
if sectionField.Name == fieldModel.GetLabel() {
oldValue := sectionField.Variable.Value
newValue := fieldModel.GetValue()
if oldValue != newValue {
cmds = append(cmds, func() tea.Msg {
return types.SugarMsg{
VariableName: sectionField.VariableName,
FieldName: sectionField.Name,
Compute: sectionField.Compute,
Focus: m.focus,
}
})
sectionField.Variable.Value = newValue //recompute вызывать.
//todo что? надо на гите посмотреть в истории, может я это уже сделал, ведь sugar Msg отправляется
}
}
}
}
//Если есть команда поля, то также помещаем её в массив команд
if fieldCmd != nil {
cmds = append(cmds, fieldCmd)
}
}
//Объединяем весь массив команду в одну
return tea.Batch(cmds...)
}
// simpleAboutRender формирует единую строку, описывающую настраиваемое приложение.
func simpleAboutRender(paragraphs []string, width int) string {
var sb strings.Builder
for _, p := range paragraphs {
p = strings.TrimSpace(p)
if p == "" {
sb.WriteString("\n")
continue
}
switch {
case strings.HasPrefix(p, "# "):
// Заголовок 1 уровня
//h1Len := utf8.RuneCountInString(p[2:])
//h1Offset := strings.Repeat(" ", (width-h1Len)/2)
//sb.WriteString(ui.AboutTitle.Render(h1Offset+p[2:]) + "\n\n")
sb.WriteString(ui.AboutTitle.Render(p[2:]) + "\n\n")
case strings.HasPrefix(p, "## "):
// Заголовок 2 уровня
sb.WriteString(ui.AboutH2.Render(p[3:]) + "\n\n")
case strings.HasPrefix(p, "### "):
// Заголовок 3 уровня
sb.WriteString(" " + ui.AboutH3.Render(p[4:]) + "\n\n")
case strings.HasPrefix(p, "- "), strings.HasPrefix(p, "* "):
// Список простой
text := strings.TrimLeft(p[2:], " ")
wrapped := wordwrap.String(text, width-10)
sb.WriteString(" " + ui.AboutBullet.Render("• ") + ui.AboutNormal.Render(wrapped) + "\n")
case strings.HasPrefix(p, "> "):
// Цитаты
text := strings.TrimPrefix(p, "> ")
wrapped := wordwrap.String(text, width-12)
sb.WriteString(" " + ui.AboutNormal.Foreground(ui.ColorGrey).Render("│ "+wrapped) + "\n")
default:
// Обычный текст
wrapped := wordwrap.String(p, width-8)
sb.WriteString(" " + ui.AboutNormal.Render(wrapped) + "\n\n")
}
}
return sb.String()
}