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" "unicode/utf8" ) type FormModel struct { CurrentSection *types.Section FieldModels []fields.FieldModel focus int renderedContent string width int height int } 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))) } } if len(fieldModels) > focus { fieldModels[focus].Focus() } m := &FormModel{ FieldModels: fieldModels, focus: focus, CurrentSection: currentSection, width: width, height: height, } if currentSection.Type == types.SectionAbout { m.renderedContent = simpleAboutRender(currentSection.Paragraphs, width) m.focus = 0 } return m } func (m *FormModel) Init() tea.Cmd { return nil } func (m *FormModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: 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": for _, field := range m.FieldModels { if field.CaptureKeys() { return m, m.updateFields(msg) } } 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": logrus.Infof("sections %s, focus %d, fields len %d", m.CurrentSection.Name, m.focus, len(m.FieldModels)) if len(m.FieldModels) == m.focus { m.focus = 0 return m, func() tea.Msg { return types.NextSectionMsg{} } } case "tab", "down", "shift+tab", "up": //Итак, тут надо учесть динамические поля. // key := msg.String() if m.focus != len(m.FieldModels) { logrus.Infof("1, focus is %d", m.focus) //todo исправить фокус field := m.FieldModels[m.focus] logrus.Infof("field %s, capture keys %t", field.GetLabel(), field.CaptureKeys()) if !field.CaptureKeys() { if key == "tab" || key == "down" { m.focus++ } else if key == "shift+tab" || key == "up" { m.focus-- } } } else { logrus.Infof("2") if key == "tab" || key == "down" { m.focus++ } else if key == "shift+tab" || key == "up" { m.focus-- } } if m.focus == len(m.FieldModels)+1 { m.focus = 0 } else if m.focus < 0 { m.focus = len(m.FieldModels) } } case types.SelectedOptionMsg: logrus.Infof("chose %+v", msg) } return m, m.updateFields(msg) } 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)) } } 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(" "), ) } 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) } } 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 вызывать. } } } } if fieldCmd != nil { cmds = append(cmds, fieldCmd) } } return tea.Batch(cmds...) } 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") 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() }