package setup import ( "bubble/setup/fields" "bubble/setup/ui" "bubble/types" "fmt" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/glamour" "github.com/charmbracelet/lipgloss" "github.com/muesli/reflow/wordwrap" "github.com/sirupsen/logrus" "strings" ) 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 { md := strings.Join(currentSection.Paragraphs, "\n\n") renderer, _ := glamour.NewTermRenderer( glamour.WithAutoStyle(), glamour.WithWordWrap(width-10)) rendered, _ := renderer.Render(md) m.renderedContent = rendered 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 { md := strings.Join(m.CurrentSection.Paragraphs, "\n\n") renderer, _ := glamour.NewTermRenderer( glamour.WithAutoStyle(), glamour.WithWordWrap(m.width-10), ) rendered, _ := renderer.Render(md) m.renderedContent = rendered } return m, nil case tea.KeyMsg: switch msg.String() { case "ctrl+c", "q": return m, tea.Quit 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) } cmd := m.updateFields(msg) return m, cmd } func (m *FormModel) View() string { contentWidth := ui.ContentWidth(m.width) var res strings.Builder for i, field := range m.FieldModels { fieldStr := wordwrap.String(field.View(), contentWidth-10) if i == m.focus { res.WriteString(ui.FormFieldFocused.Render(fieldStr)) } else { res.WriteString(ui.FormFieldUnfocused.Render(fieldStr)) } res.WriteString("\n\n") } if m.CurrentSection.Type == types.SectionAbout { res.WriteString(m.renderedContent) } nextBtn := "[ next section ]" if m.focus == len(m.FieldModels) { res.WriteString(ui.FormNextButton.Render(nextBtn)) } else { res.WriteString(ui.FormFieldUnfocused.Render(nextBtn)) } container := ui.FormContainer.Width(contentWidth).Render(res.String()) return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, container, lipgloss.WithWhitespaceChars(" "), ) //for i, field := range m.FieldModels { // fieldStr := wordwrap.String(field.View(), contentWidth-8) // if i == m.focus { // res.WriteString(ui.FormFocusedStyle.Render(fieldStr)) // } else { // res.WriteString(ui.FormUnfocusedStyle.Render(fieldStr)) // } // res.WriteString("\n\n") //} // //if m.CurrentSection.Type == types.SectionAbout { // res.WriteString("\n\n" + m.renderedContent) //} // //button := "[ next section ]" //if m.focus == len(m.FieldModels) { // res.WriteString(ui.FormFocusedStyle.Render(button)) //} else { // res.WriteString(ui.FormUnfocusedStyle.Render(button)) //} // //container := ui.FormStyle.Width(contentWidth).Render(res.String()) // //return lipgloss.Place(m.width, m.height, // lipgloss.Center, lipgloss.Top, // 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...) }