package setup import ( "bubble/bootstrap" "bubble/types" "fmt" tea "github.com/charmbracelet/bubbletea" "github.com/sirupsen/logrus" "os/exec" ) func Start() { m := NewMain() p := tea.NewProgram(m, tea.WithAltScreen()) m.p = p if _, err := p.Run(); err != nil { panic(err) } } // todo подсказка горячие клавиши // 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 //Высота окна p *tea.Program } // NewMain создаем новый объект настройки func NewMain() *Main { //Читаем все конфиги приложении apps := bootstrap.LoadApplications() m := &Main{ Applications: apps, CurrentSectionIdx: 0, } //Записываем в текущую модель список приложений 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 { //todo комментарии 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 msg.String() { case "ctrl+l": //Обработчик клавиш ctrl+l показывает/скрывает окно логирования действий m.showLog = !m.showLog 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: //Получаем сообщение о переходе на следующую секцию настраиваемого приложения. //Добавляем команду для возврата сообщения показа модального окна о выполнении действий секции //todo добавить проверку на наличие действий, если действий нет, просто переходить к следующей секции, выдавать сообщение "Действий секции нет" cmds = append(cmds, func() tea.Msg { return types.ShowModalMsg{ Type: types.ModalInfo, Title: "Выполнение действий", Message: "Ожидайте завершения действий секции", ModalButtons: nil, OnResult: nil, } }) //Запуск функции выполнения действий в горутине go m.executeSectionActions() 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 { //Если действие успешно, переходим к следующей секции. //Ищем индекс следующей секции, если он остался 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) } } 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 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) 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.showLog { view = m.log.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 } } } } } // 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 !CheckCondition(action.Condition, m.CurrentApplication) { return } sectionActionLog := fmt.Sprintf("Выполнение действия: %s", action.Name) logrus.Infof(sectionActionLog) logBuffer = append(logBuffer, sectionActionLog) //Проверяем, что все нужные пакеты для действия установлены if err := m.InstallPackage(action.RequirePackages); err != nil { errLog := fmt.Sprintf("Ошибка установки пакета: %v", err) logrus.Errorf(errLog) logBuffer = append(logBuffer, errLog) success = false break } //Выполняем действие if err := 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.p.Send(types.ActionsCompleteMsg{ Success: success, Log: logBuffer, }) } // InstallPackage проверяет установлен ли пакет, устанавливает при отсутствии func (m *Main) InstallPackage(packages []string) error { for _, pkg := range packages { cmd := exec.Command("dpkg", "-s", pkg) if err := cmd.Run(); err != nil { installCmd := exec.Command("apt", "install", "-y", pkg) out, err := installCmd.CombinedOutput() if err != nil { return fmt.Errorf("пакет %s: %v\nOutput: %s", pkg, err, out) } } } return nil }