package setup import ( "bubble/bootstrap" "bubble/types" tea "github.com/charmbracelet/bubbletea" ) func Start() { p := tea.NewProgram(NewMain(), tea.WithAltScreen()) if _, err := p.Run(); err != nil { panic(err) } } // todo подсказка горячие клавиши // Main структура описывающая объект настроек type Main struct { current tea.Model //Текущая модель для отображения overlay tea.Model //Модель для отображения модальных окон Applications []types.PackageApplication //Список доступных приложений для настройки CurrentApplication *types.PackageApplication //Выбранное приложение для настройки CurrentSection *types.Section //Текущая секция CurrentSectionIdx int //Индекс текущей секции Width int //Ширина окна Height int //Высота окна } // NewMain создаем новый объект настройки func NewMain() *Main { //Читаем все конфиги приложении apps := bootstrap.LoadApplications() m := &Main{ Applications: apps, CurrentSectionIdx: 0, } //Записываем в текущую модель список приложений m.current = NewListApplicationsModel(m.Applications, m.Width, m.Height) return m } func (m *Main) Init() tea.Cmd { return m.current.Init() } // Update отслеживает действия в объекте настроек func (m *Main) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmds []tea.Cmd var cmd tea.Cmd //Если у нас установлен модель модального окна, то передаем сообщение в его функцию обновления, //при возврате команды модального окна записываем её в массив команд. //В противном случае передаем сообщение в функцию обновления текущей модели, //при возврате команды также записываем её в массив команд. if m.overlay != nil { m.overlay, cmd = m.overlay.Update(msg) if cmd != nil { cmds = append(cmds, cmd) } } else { m.current, cmd = m.current.Update(msg) if cmd != nil { cmds = append(cmds, cmd) } } switch msg := msg.(type) { case tea.WindowSizeMsg: //Отслеживаем и записываем новые размеры окна. //Передавать сообщение в модель текущую или модального окна для вызова их обработки изменения размеров не нужно, //они получили WindowSizeMsg выше, когда мы передаем им переменную msg. m.Width = msg.Width m.Height = msg.Height case tea.KeyMsg: //Отслеживаем нажатие клавиш приложения switch msg.String() { case "ctrl+c": //Обработчик клавиш ctrl+c у приложения нужен для его закрытия. //Возвращаем команду по возврату сообщения о показе модального окна return m, func() tea.Msg { return types.ShowModalMsg{ Type: types.ModalWarning, Title: "Закрытие приложения", Message: "Вы нажали ctrl+c, вы желаете закрыть приложение?", ModalButtons: []types.ModalButton{ {Text: "Закрыть приложение", Value: true}, {Text: "Не закрывать", Value: false}, }, OnResult: func(result any) tea.Cmd { if result == true { return tea.Quit } return nil }, } } } case types.NextSectionMsg: //Получаем сообщение о переходе на следующую секцию настраиваемого приложения. //Ищем индекс следующей секции, если он остался 0, то следующие секции недоступны. //Возвращаем модальное окно с выходом к списку приложений nextSectionIndex := 0 for sectionIdx := m.CurrentSectionIdx + 1; sectionIdx < len(m.CurrentApplication.Sections); sectionIdx++ { section := m.CurrentApplication.Sections[sectionIdx] if CheckCondition(section.Condition, m.CurrentApplication) { nextSectionIndex = sectionIdx break } } if nextSectionIndex == 0 { return m, func() tea.Msg { return types.ShowModalMsg{ Type: types.ModalWarning, Title: "Ошибка секций приложения", Message: "Указанное приложение не имеет больше доступных секций для обработки", ModalButtons: []types.ModalButton{{ Text: "Вернутся", Value: true, }}, OnResult: func(result any) tea.Cmd { return func() tea.Msg { return types.BackToListMsg{} } }, } } } //Если следующая секция доступна записываем секцию и её индекс. //Выводим либо финальную форму, если секция последняя и её тип SectionFinal, либо следующую простую секцию. m.CurrentSectionIdx = nextSectionIndex m.CurrentSection = &m.CurrentApplication.Sections[nextSectionIndex] if nextSectionIndex == len(m.CurrentApplication.Sections)-1 && m.CurrentSection.Type == types.SectionFinal { m.current = NewFinalModel(m.CurrentApplication.Variables, m.Width, m.Height) } else { m.current = NewFormModel(m.CurrentApplication, m.CurrentSectionIdx, 0, m.Width, m.Height) } case types.SugarMsg: m.Sugar(msg) case types.SelectedAppMsg: //получаем сообщение о выборе приложения m.CurrentApplication = &msg.App //ищем доступную секцию for sectionIdx := 0; sectionIdx < len(m.CurrentApplication.Sections); sectionIdx++ { section := m.CurrentApplication.Sections[sectionIdx] if CheckCondition(section.Condition, m.CurrentApplication) { m.CurrentSectionIdx = sectionIdx m.CurrentSection = §ion break } } //Если секция доступная к выводу не была найдена, то возвращаем модальное окно с выходом к списку приложений if m.CurrentSection == nil { return m, func() tea.Msg { return types.ShowModalMsg{ Type: types.ModalError, Title: "Ошибка", Message: "Ошибка выбора приложения. Указанное приложение не имеет доступных секций." + "Проверьте конфиг настраиваемого приложения на предмет выполнения условий секций", ModalButtons: []types.ModalButton{{Text: "Вернуться", Value: true}}, OnResult: func(result any) tea.Cmd { return func() tea.Msg { return types.BackToListMsg{} } }, } } } //создаем новую форму m.current = NewFormModel(m.CurrentApplication, m.CurrentSectionIdx, 0, m.Width, m.Height) case types.BackToListMsg: //Получаем сообщение о возврате к списку приложений. //Стираем текущие приложение, стираем текущую секцию, стираем индекс секции, записываем в текущую модель список приложений m.CurrentApplication = nil m.CurrentSection = nil m.CurrentSectionIdx = 0 m.current = NewListApplicationsModel(m.Applications, m.Width, m.Height) case types.ShowModalMsg: //Получаем сообщение о показе модального окна. //В поле модального окна записываем новый объект модального окна на основе сообщений. //НЕ СТИРАЕМ ТЕКУЩЕЕ ПРИЛОЖЕНИЕ m.overlay = NewModalModel(msg, m.Width, m.Height) return m, nil case types.CloseModalMsg: //Получаем сообщение о закрытии модального окна. //Стираем модальное окно. m.overlay = nil return m, nil //предохранитель //return m,tea.Batch(cmds...) } //Возвращаем команду собранную из массива команд текущей модели return m, tea.Batch(cmds...) } // View формирует строку на основе модального окна или текущей модели func (m *Main) View() string { view := m.current.View() if m.overlay != nil { view = m.overlay.View() } return view } // Sugar проверяет поле указанное в сообщение на важность в форме и приложении. func (m *Main) Sugar(sugar types.SugarMsg) { //Если полей с указанной переменной несколько, то выключаем все прочие поля кроме, указанного в сообщении if m.IsMultiField(sugar.VariableName) { m.DeactivateField(sugar.VariableName, sugar.FieldName) } //Если от переменной поля зависит секции или поля, //то формируем новую форму с учетом прошлого фокуса if IsParent(sugar.VariableName, m.CurrentApplication.Sections) { m.current = NewFormModel(m.CurrentApplication, m.CurrentSectionIdx, sugar.Focus, m.Width, m.Height) } } // IsParent проверяет являться ли указанная переменная условие для поля или секции func IsParent(variableName string, sections []types.Section) bool { for _, section := range sections { for _, parent := range section.Parent { if parent == variableName { return true } } for _, field := range section.Fields { for _, parent := range field.Parent { if parent == variableName { return true } } } } return false } // IsMultiField проверяет переменную на использование несколькими полями. func (m *Main) IsMultiField(variableName string) bool { var count int for _, section := range m.CurrentApplication.Sections { for _, field := range section.Fields { if field.Name == variableName { count++ } } } return count > 1 } // DeactivateField отключает все поля использующие указанную переменную, кроме поля с указанным именем func (m *Main) DeactivateField(variableName, fieldName string) { for sectionIdx, section := range m.CurrentApplication.Sections { for fieldIdx, field := range section.Fields { if field.Variable.Name == variableName { if field.Name != fieldName { m.CurrentApplication.Sections[sectionIdx].Fields[fieldIdx].Active = false } else { m.CurrentApplication.Sections[sectionIdx].Fields[fieldIdx].Active = true } } } } }