GoSetup-bubble/setup/main.go

389 lines
17 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/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 = &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)
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
}