2025-12-26 17:45:37 +05:00
package setup
import (
"bubble/setup/fields"
"bubble/setup/ui"
2026-01-23 17:52:47 +05:00
"bubble/setup/utils"
2025-12-26 17:45:37 +05:00
"bubble/types"
"fmt"
2026-01-16 17:44:03 +05:00
"github.com/charmbracelet/bubbles/viewport"
2025-12-26 17:45:37 +05:00
tea "github.com/charmbracelet/bubbletea"
2025-12-29 17:46:48 +05:00
"github.com/charmbracelet/lipgloss"
"github.com/muesli/reflow/wordwrap"
2025-12-26 17:45:37 +05:00
"github.com/sirupsen/logrus"
"strings"
)
2026-01-15 17:55:52 +05:00
// FormModel структура описывающая модель отрисовки формы настройки
2025-12-26 17:45:37 +05:00
type FormModel struct {
2026-01-19 17:44:49 +05:00
CurrentSection * types . Section //Указатель секции с которой надо работать
FieldModels [ ] fields . FieldModel //Массив моделей полей
focus int //Фокус для отслеживания с каким объектом надо работать
content string //Строка хранения информации о приложении
viewport viewport . Model //Объект куда помещена информация о приложении для возможности прокручивать её
width int //Ширина окна
height int //Высота окна
2025-12-26 17:45:37 +05:00
}
2026-01-15 17:55:52 +05:00
// NewFormModel создает новую модель формы на основе данных настраиваемого приложения
2025-12-29 17:46:48 +05:00
func NewFormModel ( application * types . PackageApplication , sectionIdx , focus , width , height int ) * FormModel {
2026-01-15 17:55:52 +05:00
//Получаем секцию которую надо отрисовать
2025-12-26 17:45:37 +05:00
currentSection := & application . Sections [ sectionIdx ]
2026-01-15 17:55:52 +05:00
//Заполняем массив моделей полей с учетом условий
2025-12-26 17:45:37 +05:00
var fieldModels [ ] fields . FieldModel
for _ , sectionField := range currentSection . Fields {
2026-01-23 17:52:47 +05:00
if ! utils . CheckCondition ( sectionField . Condition , application ) {
2025-12-26 17:45:37 +05:00
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 != "" {
2026-01-23 17:52:47 +05:00
specialOptions := utils . SpecialValueHandle ( sectionField . SpecialValue )
2025-12-26 17:45:37 +05:00
if strSpecial , ok := specialOptions . ( [ ] string ) ; ok {
options = strSpecial
}
}
selected := fmt . Sprintf ( "%v" , sectionField . Variable . Value )
2026-01-23 17:52:47 +05:00
validationFunc := utils . ValidateField ( sectionField . Name , sectionField . VariableName , sectionField . Validate , application )
2026-01-21 17:56:21 +05:00
fieldModels = append ( fieldModels , fields . NewSelectModel ( sectionField . Name , selected , sectionField . Description , options , validationFunc ) )
2025-12-26 17:45:37 +05:00
case "check" :
checked := false
if value , ok := sectionField . Variable . Value . ( bool ) ; ok {
checked = value
} else {
checked = sectionField . Variable . Value == "true"
}
2026-01-23 17:52:47 +05:00
validationFunc := utils . ValidateCheckField ( sectionField . Name , sectionField . VariableName , sectionField . Validate , application )
2026-01-20 17:40:21 +05:00
fieldModels = append ( fieldModels , fields . NewCheckboxModel ( sectionField . Name , sectionField . Description , checked , validationFunc ) )
2025-12-26 17:45:37 +05:00
case "compute" :
2026-01-23 17:52:47 +05:00
value , err := utils . ComputeValue ( sectionField . Compute , application )
2025-12-26 17:45:37 +05:00
if err != nil {
value = fmt . Sprintf ( "Ошибка вычисления: %v" , err )
}
2026-01-19 17:44:49 +05:00
fieldModels = append ( fieldModels , fields . NewComputeModel ( sectionField . Name , fmt . Sprintf ( "%v" , value ) , sectionField . Description ) )
2025-12-26 17:45:37 +05:00
//compute расчет
case "file" :
2026-01-23 17:52:47 +05:00
validationFunc := utils . ValidateField ( sectionField . Name , sectionField . VariableName , sectionField . Validate , application )
2026-01-20 17:40:21 +05:00
fieldModels = append ( fieldModels , fields . NewFileSelectModel ( sectionField . Name , fmt . Sprintf ( "%v" , sectionField . Variable . Value ) , sectionField . Description , sectionField . FileFieldExtensions , validationFunc ) )
2025-12-26 17:45:37 +05:00
case "directory" :
2026-01-23 17:52:47 +05:00
validationFunc := utils . ValidateField ( sectionField . Name , sectionField . VariableName , sectionField . Validate , application )
2026-01-20 17:40:21 +05:00
fieldModels = append ( fieldModels , fields . NewDirSelectModel ( sectionField . Name , fmt . Sprintf ( "%v" , sectionField . Variable . Value ) , sectionField . Description , validationFunc ) )
2025-12-26 17:45:37 +05:00
default :
2026-01-23 17:52:47 +05:00
validationFunc := utils . ValidateField ( sectionField . Name , sectionField . VariableName , sectionField . Validate , application )
2026-01-20 17:40:21 +05:00
fieldModels = append ( fieldModels , fields . NewInputModel ( sectionField . Name , fmt . Sprintf ( "%v" , sectionField . Variable . Value ) , sectionField . Description , validationFunc ) )
2025-12-26 17:45:37 +05:00
}
}
2026-01-15 17:55:52 +05:00
//Устанавливаем фокус (фокус может быть не равен нулю, если новая форма сформирована после Sugar события, который обновляет форму в ТЕКУЩЕМ состоянии)
2025-12-26 17:45:37 +05:00
if len ( fieldModels ) > focus {
fieldModels [ focus ] . Focus ( )
}
m := & FormModel {
FieldModels : fieldModels ,
focus : focus ,
CurrentSection : currentSection ,
2025-12-29 17:46:48 +05:00
width : width ,
height : height ,
2025-12-26 17:45:37 +05:00
}
2026-01-16 17:44:03 +05:00
////Формируем описание приложения если секция установлена About
//if currentSection.Type == types.SectionAbout {
// m.renderedContent = simpleAboutRender(currentSection.Paragraphs, width)
// m.focus = 0
//}
if width > 0 && height > 0 {
m . initViewport ( width , height )
2025-12-26 17:45:37 +05:00
}
return m
}
func ( m * FormModel ) Init ( ) tea . Cmd {
return nil
}
2026-01-15 17:55:52 +05:00
// Update отслеживает действия с формой
2025-12-26 17:45:37 +05:00
func ( m * FormModel ) Update ( msg tea . Msg ) ( tea . Model , tea . Cmd ) {
2026-01-14 18:08:43 +05:00
2025-12-26 17:45:37 +05:00
switch msg := msg . ( type ) {
2025-12-29 17:46:48 +05:00
case tea . WindowSizeMsg :
2026-01-22 17:54:42 +05:00
//Отслеживаем и записываем новые размеры окна, обновляем область действий
2025-12-29 17:46:48 +05:00
m . width = msg . Width
m . height = msg . Height
2026-01-16 17:44:03 +05:00
m . initViewport ( m . width , m . height )
2025-12-29 17:46:48 +05:00
return m , nil
2025-12-26 17:45:37 +05:00
case tea . KeyMsg :
2026-01-15 17:55:52 +05:00
//Отслеживаем нажатие клавиш формы
2025-12-26 17:45:37 +05:00
switch msg . String ( ) {
case "esc" :
2026-01-15 17:55:52 +05:00
//Обработчик клавиши escape на форме нужен для закрытия полей поддерживающих открытие.
//Обходим все доступные в текущей форме поля и закрываем те, что были открыты.
//Проверять на фокус не нужно, так как не предполагается, что у нас может быть открыто сразу несколько полей.
//Поэтому просто возвращаем команду по возврату сообщение о б обновлении полей.
2025-12-29 17:46:48 +05:00
for _ , field := range m . FieldModels {
2026-01-15 17:55:52 +05:00
//проверяем открыто ли поле
2025-12-29 17:46:48 +05:00
if field . CaptureKeys ( ) {
2026-01-15 17:55:52 +05:00
//обновляем поля если оно открыто
2025-12-29 17:46:48 +05:00
return m , m . updateFields ( msg )
}
}
2026-01-15 17:55:52 +05:00
//Если открытых полей не было найдено, значит escape была нажата для закрытия настройки,
//возвращаем команду по возврату сообщения о б открытии модального окна
2025-12-29 17:46:48 +05:00
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
} ,
}
}
2025-12-26 17:45:37 +05:00
case "enter" :
2026-01-15 17:55:52 +05:00
//Обработчик клавиши enter на форме нужен для перехода на следующую секцию.
//Проверяем где сейчас находится фокус, если е г о позиция равняется количеству полей,
//значит он вышел за пределы формы и находится на кнопке next section, что ставится последней.
//После этого мы сбрасываем фокус и возвращаем команду по возврату сообщения о переходе на следующую секцию
2025-12-26 17:45:37 +05:00
if len ( m . FieldModels ) == m . focus {
2025-12-29 17:46:48 +05:00
m . focus = 0
2025-12-26 17:45:37 +05:00
return m , func ( ) tea . Msg {
return types . NextSectionMsg { }
}
}
2026-01-15 17:55:52 +05:00
case "tab" , "shift+tab" , "down" , "up" :
//Обработчик клавиш tab, shift+tab, down, up на форме нужен для перехода между объектами формы.
//Получаем точное имя клавиши нажатия, после проверяем где сейчас находится фокус.
2026-01-16 17:44:03 +05:00
//Если фокус меньше количества полей, то значит фокус сейчас на поле формы.
// Подробнее. У нас фокус в двух состояниях так сказать,
// фокус равен количество полей, значит он на кнопке следующей секции,
// если меньше количества полей или равен 0, значит фокус на поле формы.
// Если у нас нет полей на форме (секция About), значит количество полей равно 0, а фокус по умолчанию устанавливается 0.
2026-01-23 17:52:47 +05:00
// Чтобы m.FieldModels[m.focus] не вызвало ошибку "индекс вне массива",
2026-01-16 17:44:03 +05:00
// нужно чтобы в условие m.focus < len(m.FieldModels) попал фокус меньше 0 (-1,-2 и тд).
// Чтобы этого избежать, есть проверка, которая устанавливает m.focus на последнюю позицию (количество полей на форме).
// Чтобы эта проверка дала сбой, нужно после неё прописать изменение фокуса формы на отрицательный принудительно, так как такое не может произойти в текущем коде.
2026-01-15 17:55:52 +05:00
//Получаем имя поля (так как это старый фокус, то и поле на котором мы находимся это предыдущее поле, отсюда и название переменной),
//Если поле не открыто, то мы можем перейти на следующее поле, увеличив или уменьшив фокус.
//Если фокус равен количеству полей, значит он находится на кнопке переключения секций. Увеличиваем или уменьшаем фокус.
2025-12-26 17:45:37 +05:00
key := msg . String ( )
2026-01-16 17:44:03 +05:00
if m . focus < len ( m . FieldModels ) {
2026-01-15 17:55:52 +05:00
prevField := m . FieldModels [ m . focus ]
if ! prevField . CaptureKeys ( ) {
2025-12-26 17:45:37 +05:00
if key == "tab" || key == "down" {
m . focus ++
} else if key == "shift+tab" || key == "up" {
m . focus --
}
2026-01-19 17:44:49 +05:00
} else {
return m , m . updateFields ( msg )
2025-12-26 17:45:37 +05:00
}
} else {
if key == "tab" || key == "down" {
m . focus ++
} else if key == "shift+tab" || key == "up" {
m . focus --
}
}
2026-01-15 17:55:52 +05:00
//Проверка правильности фокуса, чтобы не выйти за границы массива полей формы, необходимо проверить новое значение фокуса.
//Если фокус стал больше чем количество полей формы, значит он вышел за пределы всей формы и находится уже даже ниже кнопки,
2026-01-20 17:40:21 +05:00
//переводим е г о на первую позицию, то есть на 0 и перемещаем видимую область наверх.
2026-01-15 17:55:52 +05:00
//Если фокус стал меньше 0, значит он вышел за пределы формы и находится выше первого поля формы,
2026-01-20 17:40:21 +05:00
//переводим е г о на последнюю позицию, то есть на кнопку переключения секции, равную положение которой = количество полей на форме и перемещаем видимую область вниз.
2025-12-26 17:45:37 +05:00
if m . focus == len ( m . FieldModels ) + 1 {
m . focus = 0
2026-01-20 17:40:21 +05:00
return m , func ( ) tea . Msg {
return types . ViewportScrollMsg {
Direction : "up" ,
}
}
2025-12-26 17:45:37 +05:00
} else if m . focus < 0 {
m . focus = len ( m . FieldModels )
2026-01-20 17:40:21 +05:00
return m , func ( ) tea . Msg {
return types . ViewportScrollMsg {
Direction : "down" ,
}
}
2025-12-26 17:45:37 +05:00
}
}
2026-01-20 17:40:21 +05:00
case types . ViewportScrollMsg :
if msg . Direction == "up" {
m . viewport . GotoTop ( )
} else {
m . viewport . GotoBottom ( )
}
2025-12-26 17:45:37 +05:00
}
2026-01-19 17:44:49 +05:00
var viewportCmd , fieldsCmd tea . Cmd
m . viewport , viewportCmd = m . viewport . Update ( msg )
fieldsCmd = m . updateFields ( msg )
2026-01-23 17:52:47 +05:00
return m , tea . Sequence ( viewportCmd , fieldsCmd )
2025-12-26 17:45:37 +05:00
}
func ( m * FormModel ) View ( ) string {
2026-01-16 17:44:03 +05:00
2026-01-19 17:44:49 +05:00
containerWidth , containerHeight , contentWidth , _ := ui . ContentSizes ( m . width , m . height )
m . rebuildContent ( contentWidth )
2026-01-16 17:44:03 +05:00
2026-01-14 18:08:43 +05:00
container := ui . AppContainer .
Width ( containerWidth ) .
Height ( containerHeight ) .
2026-01-19 17:44:49 +05:00
Render ( m . viewport . View ( ) )
2026-01-14 18:08:43 +05:00
return lipgloss . Place (
m . width , m . height ,
2025-12-29 17:46:48 +05:00
lipgloss . Center , lipgloss . Center ,
container ,
2026-01-19 17:44:49 +05:00
lipgloss . WithWhitespaceChars ( " " ) ,
2025-12-29 17:46:48 +05:00
)
2026-01-19 17:44:49 +05:00
}
2026-01-16 17:44:03 +05:00
2026-01-19 17:44:49 +05:00
func ( m * FormModel ) initViewport ( width , height int ) {
_ , _ , contentWidth , contentHeight := ui . ContentSizes ( width , height )
2026-01-20 17:40:21 +05:00
m . viewport = viewport . New ( contentWidth , contentHeight )
2026-01-19 17:44:49 +05:00
m . viewport . SetHorizontalStep ( 1 )
2026-01-20 17:40:21 +05:00
m . rebuildContent ( contentWidth )
2026-01-19 17:44:49 +05:00
}
func ( m * FormModel ) rebuildContent ( width int ) {
var content string
if m . CurrentSection . Type == types . SectionAbout {
content = simpleAboutRender ( m . CurrentSection . Paragraphs , width )
} else {
content = simpleForm ( m . FieldModels , m . focus , width )
}
nextBtn := "[ next section ]"
if m . focus == len ( m . FieldModels ) {
nextBtn = ui . FormNextButton . Render ( nextBtn )
} else {
nextBtn = ui . FormFieldUnfocused . Render ( nextBtn )
}
fullContent := lipgloss . JoinVertical ( lipgloss . Left , content , nextBtn )
m . content = fullContent
m . viewport . SetContent ( m . content )
}
func simpleForm ( fields [ ] fields . FieldModel , focus , width int ) string {
var sb strings . Builder
for i , field := range fields {
//Строка с учетом переноса поля.
//Имеется ввиду, что если вводимое значение слишком длинное, то оно будет делиться на несколько строк.
fieldStroke := wordwrap . String ( field . View ( ) , width )
//Если фокус равен текущему полю, то выделяем е г о .
2026-01-20 17:40:21 +05:00
var finalStroke string
2026-01-19 17:44:49 +05:00
if i == focus {
2026-01-20 17:40:21 +05:00
finalStroke = ui . FormFieldFocused . Render ( fieldStroke )
} else {
finalStroke = ui . FormFieldUnfocused . Render ( fieldStroke )
}
if i != len ( fields ) - 1 {
finalStroke += "\n\n"
2026-01-19 17:44:49 +05:00
} else {
2026-01-20 17:40:21 +05:00
finalStroke += "\n"
2026-01-19 17:44:49 +05:00
}
2026-01-20 17:40:21 +05:00
sb . WriteString ( finalStroke )
2026-01-19 17:44:49 +05:00
}
return sb . String ( )
}
// simpleAboutRender формирует единую строку, описывающую настраиваемое приложение.
func simpleAboutRender ( paragraphs [ ] string , width int ) string {
var sb strings . Builder
2026-01-20 17:40:21 +05:00
for i , p := range paragraphs {
2026-01-19 17:44:49 +05:00
p = strings . TrimSpace ( p )
if p == "" {
sb . WriteString ( "\n" )
continue
}
2026-01-20 17:40:21 +05:00
var finalStroke , strokeType string
2026-01-19 17:44:49 +05:00
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")
text := strings . TrimPrefix ( p , "# " )
wrapped := wordwrap . String ( text , width )
2026-01-20 17:40:21 +05:00
finalStroke = ui . AboutTitle . Render ( wrapped )
strokeType = "header1"
2026-01-19 17:44:49 +05:00
case strings . HasPrefix ( p , "## " ) :
// Заголовок 2 уровня
text := strings . TrimPrefix ( p , "## " )
wrapped := wordwrap . String ( text , width )
2026-01-20 17:40:21 +05:00
finalStroke = ui . AboutH2 . Render ( wrapped )
strokeType = "header2"
2026-01-19 17:44:49 +05:00
case strings . HasPrefix ( p , "### " ) :
// Заголовок 3 уровня
text := strings . TrimPrefix ( p , "### " )
wrapped := wordwrap . String ( text , width )
2026-01-20 17:40:21 +05:00
finalStroke = ui . AboutH3 . Render ( wrapped )
strokeType = "header3"
2026-01-19 17:44:49 +05:00
case strings . HasPrefix ( p , "- " ) , strings . HasPrefix ( p , "* " ) :
// Список простой
text := strings . TrimLeft ( p [ 2 : ] , " " )
wrapped := wordwrap . String ( text , width )
2026-01-20 17:40:21 +05:00
finalStroke = " " + ui . AboutBullet . Render ( "• " ) + ui . AboutNormal . Render ( wrapped )
strokeType = "bullet"
2026-01-19 17:44:49 +05:00
case strings . HasPrefix ( p , "> " ) :
// Цитаты
text := strings . TrimPrefix ( p , "> " )
wrapped := wordwrap . String ( text , width )
2026-01-20 17:40:21 +05:00
finalStroke = " " + ui . AboutNormal . Foreground ( ui . ColorGrey ) . Render ( "│ " + wrapped )
strokeType = "quote"
2026-01-19 17:44:49 +05:00
default :
// Обычный текст
wrapped := wordwrap . String ( p , width )
2026-01-20 17:40:21 +05:00
finalStroke = " " + ui . AboutNormal . Render ( wrapped )
strokeType = "normal"
2026-01-19 17:44:49 +05:00
}
2026-01-20 17:40:21 +05:00
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 )
2026-01-19 17:44:49 +05:00
}
return sb . String ( )
2025-12-26 17:45:37 +05:00
}
2026-01-15 17:55:52 +05:00
// updateFields обновляет поля и их внутренние данные, формирует единую команду действий для всех полей, в том числе возврат SugarMsg
2025-12-26 17:45:37 +05:00
func ( m * FormModel ) updateFields ( msg tea . Msg ) tea . Cmd {
2026-01-15 17:55:52 +05:00
//Массив команд для выполнения
2025-12-26 17:45:37 +05:00
var cmds [ ] tea . Cmd
for i , fieldModel := range m . FieldModels {
var fieldCmd tea . Cmd
var updated fields . FieldModel
2026-01-15 17:55:52 +05:00
//Если фокус установлен на текущем поле, то нужно активировать е г о и передать ему сообщение для обновления.
//Если фокус не установлен на текущем поле, то снимаем с него фокус и передаем ему сообщение не связанное с нажатием кнопок клавиатуры
if i == m . focus {
2025-12-26 17:45:37 +05:00
fieldModel . Focus ( )
updated , fieldCmd = fieldModel . Update ( msg )
2026-01-15 17:55:52 +05:00
} else {
2025-12-26 17:45:37 +05:00
fieldModel . Blur ( )
if _ , ok := msg . ( tea . KeyMsg ) ; ! ok {
updated , fieldCmd = fieldModel . Update ( msg )
}
}
2026-01-15 17:55:52 +05:00
//Если поле успешно вернуло обновленное поле, то необходимо:
//обновить модель поля в текущей форме;
2026-01-16 17:44:03 +05:00
//проверить у поля не типа Compute изменилось ли значение переменной в настройках приложения,
2026-01-15 17:55:52 +05:00
//если изменилось, в массив команд помещаем команду по возврату сообщения Sugar
2025-12-26 17:45:37 +05:00
if updated != nil {
m . FieldModels [ i ] = updated
for _ , sectionField := range m . CurrentSection . Fields {
2026-01-16 17:44:03 +05:00
if sectionField . Name == fieldModel . GetLabel ( ) && sectionField . FieldType != "compute" {
2025-12-26 17:45:37 +05:00
oldValue := sectionField . Variable . Value
newValue := fieldModel . GetValue ( )
2026-01-16 17:44:03 +05:00
logrus . Debugf ( "label %s, oldValue(%v,%T), newValue(%v,%T), equal %t, field info %+v" ,
sectionField . Name , oldValue , oldValue , newValue , newValue , oldValue == newValue , sectionField ,
)
2025-12-26 17:45:37 +05:00
if oldValue != newValue {
2026-01-16 17:44:03 +05:00
sectionField . Variable . Value = newValue //Записываем в переменную новое значение
2025-12-26 17:45:37 +05:00
cmds = append ( cmds , func ( ) tea . Msg {
return types . SugarMsg {
VariableName : sectionField . VariableName ,
FieldName : sectionField . Name ,
Focus : m . focus ,
}
} )
2026-01-16 17:44:03 +05:00
2025-12-26 17:45:37 +05:00
}
}
}
}
2026-01-15 17:55:52 +05:00
//Если есть команда поля, то также помещаем её в массив команд
2025-12-26 17:45:37 +05:00
if fieldCmd != nil {
cmds = append ( cmds , fieldCmd )
}
}
2026-01-15 17:55:52 +05:00
//Объединяем весь массив команду в одну
2026-01-23 17:52:47 +05:00
return tea . Sequence ( cmds ... )
2025-12-26 17:45:37 +05:00
}