package setup import ( "bubble/bootstrap" "bubble/setup/utils" "bubble/types" "fmt" "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/sirupsen/logrus" ) func Start() { //Создаем новый объект Main для настройки. m := NewMain() p := tea.NewProgram(m, tea.WithAltScreen()) //Запускаем через bubble tea программу m.Program = p //Указываем объекту настроек, программу в которой запущен, чтобы передавать туда сигналы if _, err := p.Run(); err != nil { panic(err) } } // Main структура описывающая объект настроек type Main struct { current tea.Model //Текущая модель для отображения overlay tea.Model //Модель для отображения модальных окон log tea.Model //Модель для вывода логов showLog bool //Показывать ли лог Applications []types.PackageApplication //Список доступных приложений для настройки CurrentApplication *types.PackageApplication //Выбранное приложение для настройки CurrentSection *types.Section //Текущая секция CurrentSectionIdx int //Индекс текущей секции Width int //Ширина окна Height int //Высота окна Program *tea.Program //Программа для отправки данных между собой Keys BaseKeyMap help help.Model } // NewMain создаем новый объект настройки func NewMain() *Main { //Читаем все конфиги приложении apps := bootstrap.LoadApplications() m := &Main{ Applications: apps, CurrentSectionIdx: 0, Keys: keys, help: help.New(), } m.help.ShowAll = false //Записываем в текущую модель список приложений m.current = NewListApplicationsModel(m.Applications, m.Width, m.Height) m.log = NewLogModel(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 if m.showLog { m.log, cmd = m.log.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 if m.log != nil { m.log, cmd = m.log.Update(msg) if cmd != nil { cmds = append(cmds, cmd) } } case tea.KeyMsg: //Отслеживаем нажатие клавиш приложения switch { case key.Matches(msg, m.Keys.Help): //Обработчик клавиш помощи, показывает/скрывает полное меню подсказок m.help.ShowAll = !m.help.ShowAll case key.Matches(msg, m.Keys.Log): //Обработчик клавиш логов показывает/скрывает окно логирования действий m.showLog = !m.showLog case key.Matches(msg, m.Keys.Quit): //Обработчик клавиш закрытия приложения. //Возвращаем команду по возврату сообщения о показе модального окна 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: //Получаем сообщение о переходе на следующую секцию настраиваемого приложения. //Если у текущей секции есть действия, то добавляем в массив команд функцию возврата сообщения показа модального окна с информацией о запуске действий. //Запускаем функцию выполнения действий секции в горутине. //Если действий у текущей секции нет, то просто меняем секцию на следующую if len(m.CurrentSection.Actions) > 0 { cmds = append(cmds, func() tea.Msg { return types.ShowModalMsg{ Type: types.ModalLoading, Title: "Выполнение действий", Message: "Ожидайте завершения действий секции", ModalButtons: nil, OnResult: nil, } }) //Запуск функции выполнения действий в горутине go m.executeSectionActions() } else { if ok, modalMsg := m.NextSectionProcess(); !ok { return m, func() tea.Msg { return modalMsg } } } case types.ActionsCompleteMsg: //Команда закрытия модального окна cmds = append(cmds, func() tea.Msg { return types.CloseModalMsg{} }) //Обновляем данные модели логирования logModel := m.log.(*LogModel) logModel.logs = append(logModel.logs, msg.Log...) m.log, cmd = logModel.Update(msg) if msg.Success { //Если действие успешно, то запускаем функцию перехода на следующую секцию. if ok, modalMsg := m.NextSectionProcess(); !ok { return m, func() tea.Msg { return modalMsg } } } else { //Если действие не удалось выполнить показываем модель логов принудительно, //также возвращаем команду возврата сообщения показа модального окна m.showLog = true cmds = append(cmds, func() tea.Msg { return types.ShowModalMsg{ Type: types.ModalError, Title: "Ошибка выполнения действий", Message: "Ошибка выполнения действий секций, смотри логи", ModalButtons: []types.ModalButton{ {Text: "OK", Value: true}, }, OnResult: func(result any) tea.Cmd { return nil }, } }) } 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 utils.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.log, cmd = m.log.Update(types.ActionClearLogsMsg{}) 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) initModalCmd := m.overlay.Init() if initModalCmd != nil { cmds = append(cmds, initModalCmd) } return m, tea.Sequence(cmds...) case types.CloseModalMsg: //Получаем сообщение о закрытии модального окна. //Стираем модальное окно. m.overlay = nil return m, nil //предохранитель //return m,tea.Sequence(cmds...) } //Возвращаем команду собранную из массива команд текущей модели return m, tea.Sequence(cmds...) } // View формирует строку на основе модального окна или текущей модели func (m *Main) View() string { view := m.current.View() if m.showLog { view = m.log.View() } if m.overlay != nil { view = m.overlay.View() } return view + "\n\n" + m.help.View(m.Keys) } // Sugar проверяет поле указанное в сообщение на важность в форме и приложении. func (m *Main) Sugar(sugar types.SugarMsg) { //Если полей с указанной переменной несколько, то выключаем все прочие поля кроме, указанного в сообщении if utils.IsMultiField(sugar.VariableName, m.CurrentApplication.Sections) { m.DeactivateField(sugar.VariableName, sugar.FieldName) } //Если от переменной поля зависит секции или поля, //то формируем новую форму с учетом прошлого фокуса if utils.IsParent(sugar.VariableName, m.CurrentApplication.Sections) { m.current = NewFormModel(m.CurrentApplication, m.CurrentSectionIdx, sugar.Focus, m.Width, m.Height) } } // 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 } } } } } // executeSectionActions запускает выполнение действий текущей секции func (m *Main) executeSectionActions() { var logBuffer []string success := true sectionLog := fmt.Sprintf("Выполнение действий секции %s, количество действий %d", m.CurrentSection.Name, len(m.CurrentSection.Actions)) logrus.Infof(sectionLog) logBuffer = append(logBuffer, sectionLog) for _, action := range m.CurrentSection.Actions { //Если условие для действия не выполняется, то пропускаем if !utils.CheckCondition(action.Condition, m.CurrentApplication) { return } sectionActionLog := fmt.Sprintf("Выполнение действия: %s", action.Name) logrus.Infof(sectionActionLog) logBuffer = append(logBuffer, sectionActionLog) //Проверяем, что все нужные пакеты для действия установлены if err := utils.InstallPackages(action.RequirePackages); err != nil { errLog := fmt.Sprintf("Ошибка установки пакета: %v", err) logrus.Errorf(errLog) logBuffer = append(logBuffer, errLog) success = false break } //Выполняем действие if err := utils.ExecuteAction(&action, m.CurrentApplication); err != nil { errLog := fmt.Sprintf("Ошибка выполнения действия: %v", err) logrus.Errorf(errLog) logBuffer = append(logBuffer, errLog) success = false break } successMsg := fmt.Sprintf("Действие %s успешно выполнено", action.Name) logBuffer = append(logBuffer, successMsg) logrus.Infof(successMsg) } //Отправляем в программу сигнал об успешном завершении действия m.Program.Send(types.ActionsCompleteMsg{ Success: success, Log: logBuffer, }) } // NextSectionProcess проверяет доступность следующей секции для настройки func (m *Main) NextSectionProcess() (bool, types.ShowModalMsg) { //Ищем индекс следующей секции, если он остался 0, то следующие секции недоступны. //Возвращаем модальное окно с выходом к списку приложений nextSectionIndex := 0 for sectionIDx := m.CurrentSectionIdx + 1; sectionIDx < len(m.CurrentApplication.Sections); sectionIDx++ { section := m.CurrentApplication.Sections[sectionIDx] if utils.CheckCondition(section.Condition, m.CurrentApplication) { nextSectionIndex = sectionIDx break } } if nextSectionIndex == 0 { return false, 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) } return true, types.ShowModalMsg{} }