GoSetup-bubble/setup/main.go

382 lines
18 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package setup
import (
"bubble/bootstrap"
"bubble/setup/utils"
"bubble/types"
"fmt"
"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 //Программа для отправки данных между собой
}
// 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 {
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, types.MainKeys.Help):
//Обработчик клавиш помощи, показывает/скрывает полное меню подсказок
case key.Matches(msg, types.MainKeys.Log):
//Обработчик клавиш логов, показывает/скрывает окно логирования действий
m.showLog = !m.showLog
case key.Matches(msg, types.MainKeys.Exit):
//Обработчик клавиш закрытия приложения.
//Возвращаем команду по возврату сообщения о показе модального окна
return m, func() tea.Msg {
return types.ShowModalMsg{
Type: types.ModalWarning,
Title: "Закрытие приложения",
Message: "Вы нажали ctrl+c/ctrl+q, вы желаете закрыть приложение?",
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 = &section
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(MainKeys)
return view
}
// Sugar проверяет поле указанное в сообщение на важность в форме и приложении.
func (m *Main) Sugar(sugar types.SugarMsg) {
//Если полей с указанной переменной несколько, то выключаем все прочие поля кроме, указанного в сообщении. todo изменение поля со старта приводит к сбросу в конец
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) {
continue
}
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 && m.CurrentSectionIdx+1 == len(m.CurrentApplication.Sections) {
return false, types.ShowModalMsg{
Type: types.ModalInfo,
Title: "Настройка завершена",
Message: "Настройка завершена, нажмите enter чтобы закрыть приложение",
ModalButtons: []types.ModalButton{
{Text: "Закрыть приложение", Value: true},
},
OnResult: func(result any) tea.Cmd {
return func() tea.Msg {
return tea.Quit()
}
},
}
} else 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{}
}