2025-12-29 17:46:48 +05:00
|
|
|
|
package setup
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"bubble/setup/ui"
|
|
|
|
|
|
"bubble/types"
|
2026-01-23 17:52:47 +05:00
|
|
|
|
"github.com/charmbracelet/bubbles/spinner"
|
2025-12-29 17:46:48 +05:00
|
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
|
|
|
|
"github.com/charmbracelet/lipgloss"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-01-15 17:55:52 +05:00
|
|
|
|
// ModalModel структура описывающая модель отрисовки модального окна
|
2025-12-29 17:46:48 +05:00
|
|
|
|
type ModalModel struct {
|
2026-01-15 17:55:52 +05:00
|
|
|
|
modalType types.ModalType //Тип модального окна
|
|
|
|
|
|
title string //Заголовок модального окна
|
|
|
|
|
|
message string //Сообщение модального окна
|
|
|
|
|
|
buttons []types.ModalButton //Кнопки модального окна
|
|
|
|
|
|
focus int //Фокус для отслеживания с каким объектом надо работать
|
|
|
|
|
|
width int //Ширина окна
|
|
|
|
|
|
height int //Высота окна
|
|
|
|
|
|
onResult func(result any) tea.Cmd //Какое действие надо выполнить при нажатии
|
2026-01-23 17:52:47 +05:00
|
|
|
|
spinner spinner.Model //Для демонстрации прогресса выполнения действий
|
2025-12-29 17:46:48 +05:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 17:55:52 +05:00
|
|
|
|
// NewModalModel создает новую модель модального окна на основе переданного сообщения
|
2025-12-29 17:46:48 +05:00
|
|
|
|
func NewModalModel(msg types.ShowModalMsg, width, height int) *ModalModel {
|
2026-01-23 17:52:47 +05:00
|
|
|
|
m := &ModalModel{
|
2025-12-29 17:46:48 +05:00
|
|
|
|
modalType: msg.Type,
|
|
|
|
|
|
title: msg.Title,
|
|
|
|
|
|
message: msg.Message,
|
|
|
|
|
|
buttons: msg.ModalButtons,
|
|
|
|
|
|
focus: 0,
|
|
|
|
|
|
width: width,
|
|
|
|
|
|
height: height,
|
|
|
|
|
|
onResult: msg.OnResult,
|
|
|
|
|
|
}
|
2026-01-23 17:52:47 +05:00
|
|
|
|
if msg.Type == types.ModalLoading {
|
|
|
|
|
|
s := spinner.New()
|
|
|
|
|
|
s.Spinner = spinner.Line
|
|
|
|
|
|
s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
|
|
|
|
|
|
m.spinner = s
|
|
|
|
|
|
}
|
|
|
|
|
|
return m
|
2025-12-29 17:46:48 +05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (m *ModalModel) Init() tea.Cmd {
|
2026-01-23 17:52:47 +05:00
|
|
|
|
if m.modalType == types.ModalLoading {
|
|
|
|
|
|
return m.spinner.Tick
|
|
|
|
|
|
}
|
2025-12-29 17:46:48 +05:00
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 17:55:52 +05:00
|
|
|
|
// Update отслеживает действия с модальным окном
|
2025-12-29 17:46:48 +05:00
|
|
|
|
func (m *ModalModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
2026-01-23 17:52:47 +05:00
|
|
|
|
var cmd tea.Cmd
|
2025-12-29 17:46:48 +05:00
|
|
|
|
switch msg := msg.(type) {
|
|
|
|
|
|
case tea.WindowSizeMsg:
|
2026-01-15 17:55:52 +05:00
|
|
|
|
//Отслеживаем и записываем новые размеры окна
|
2025-12-29 17:46:48 +05:00
|
|
|
|
m.width = msg.Width
|
|
|
|
|
|
m.height = msg.Height
|
|
|
|
|
|
return m, nil
|
|
|
|
|
|
case tea.KeyMsg:
|
2026-01-15 17:55:52 +05:00
|
|
|
|
//Отслеживаем нажатие клавиш модального окна
|
2025-12-29 17:46:48 +05:00
|
|
|
|
switch msg.String() {
|
2026-01-15 17:55:52 +05:00
|
|
|
|
case "left", "shift+tab":
|
|
|
|
|
|
//Обработчик нажатия клавиш left и shift+tab нужен для перевода фокуса на прошлую кнопку.
|
|
|
|
|
|
//Уменьшаем фокус, если фокус стал меньше 0, значит он вышел за пределы кнопок,
|
|
|
|
|
|
//переводим его на последнюю кнопку.
|
2025-12-29 17:46:48 +05:00
|
|
|
|
m.focus--
|
|
|
|
|
|
if m.focus < 0 {
|
|
|
|
|
|
m.focus = len(m.buttons) - 1
|
|
|
|
|
|
}
|
2026-01-15 17:55:52 +05:00
|
|
|
|
case "right", "tab":
|
|
|
|
|
|
//Обработчик нажатия клавиш right и tab нужен для перевода фокуса на следующую кнопку.
|
|
|
|
|
|
//Увеличиваем фокус, если фокус стал равен количество кнопок, значит он вышел за их пределы,
|
|
|
|
|
|
//переводим его на первую позицию, то есть на 0
|
2025-12-29 17:46:48 +05:00
|
|
|
|
m.focus++
|
|
|
|
|
|
if m.focus >= len(m.buttons) {
|
|
|
|
|
|
m.focus = 0
|
|
|
|
|
|
}
|
2026-01-23 17:52:47 +05:00
|
|
|
|
case "enter":
|
2026-01-15 17:55:52 +05:00
|
|
|
|
//Обработчик клавиши enter нужен для подтверждения кнопки.
|
2026-01-23 17:52:47 +05:00
|
|
|
|
//Если у модального окна есть кнопки.
|
|
|
|
|
|
//Получаем значение нажатой кнопки, а после если у модального окна прописана функция,
|
2026-01-15 17:55:52 +05:00
|
|
|
|
//то передаем значение кнопки в функцию модального окна.
|
2026-01-23 17:52:47 +05:00
|
|
|
|
if len(m.buttons) > 0 {
|
|
|
|
|
|
result := m.buttons[m.focus].Value
|
|
|
|
|
|
if m.onResult != nil {
|
|
|
|
|
|
cmd = m.onResult(result)
|
|
|
|
|
|
}
|
|
|
|
|
|
//Возвращаем единую команду составленную из двух,
|
|
|
|
|
|
//результата выполнения функции модального окна,
|
|
|
|
|
|
//функцию возвращения сообщения о закрытии модального окна.
|
|
|
|
|
|
return m, tea.Sequence(cmd, func() tea.Msg { return types.CloseModalMsg{} })
|
2025-12-29 17:46:48 +05:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-23 17:52:47 +05:00
|
|
|
|
//Если это модальное окно загрузки, то обновляем счетчик
|
|
|
|
|
|
if m.modalType == types.ModalLoading {
|
|
|
|
|
|
m.spinner, cmd = m.spinner.Update(msg)
|
|
|
|
|
|
return m, cmd
|
|
|
|
|
|
}
|
2025-12-29 17:46:48 +05:00
|
|
|
|
return m, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 17:55:52 +05:00
|
|
|
|
// View формирует строку для вывода модального окна и её контента
|
2025-12-29 17:46:48 +05:00
|
|
|
|
func (m *ModalModel) View() string {
|
2026-01-15 17:55:52 +05:00
|
|
|
|
//Данные о размерах контейнера
|
2026-01-14 18:08:43 +05:00
|
|
|
|
containerWidth, containerHeight, _, _ := ui.ContentSizes(m.width, m.height)
|
2026-01-15 17:55:52 +05:00
|
|
|
|
//Модальное окно не должно занимать все пространство, поэтому получаем минимально допустимые размеры
|
2026-01-14 18:08:43 +05:00
|
|
|
|
modalWidth := min(containerWidth, ui.ModalMaxWidth)
|
|
|
|
|
|
modalHeight := min(containerHeight, ui.ModalMaxHeight)
|
2026-01-15 17:55:52 +05:00
|
|
|
|
//Формируем просто сообщение и кнопки с учетом фокуса
|
2026-01-14 18:08:43 +05:00
|
|
|
|
message := lipgloss.NewStyle().Render(m.message)
|
|
|
|
|
|
var buttons []string
|
|
|
|
|
|
for i, button := range m.buttons {
|
2025-12-29 17:46:48 +05:00
|
|
|
|
style := ui.ModalButtonNormal
|
|
|
|
|
|
if i == m.focus {
|
|
|
|
|
|
style = ui.ModalButtonFocused
|
|
|
|
|
|
}
|
2026-01-14 18:08:43 +05:00
|
|
|
|
buttons = append(buttons, style.Render(button.Text))
|
2025-12-29 17:46:48 +05:00
|
|
|
|
}
|
2026-01-15 17:55:52 +05:00
|
|
|
|
//Строка кнопок
|
2025-12-29 17:46:48 +05:00
|
|
|
|
buttonRow := lipgloss.JoinHorizontal(lipgloss.Center, buttons...)
|
2026-01-15 17:55:52 +05:00
|
|
|
|
//Тело контента модального окна
|
2026-01-14 18:08:43 +05:00
|
|
|
|
modalBody := lipgloss.JoinVertical(lipgloss.Center, message, "", buttonRow)
|
2026-01-23 17:52:47 +05:00
|
|
|
|
if m.modalType == types.ModalLoading {
|
|
|
|
|
|
modalBody = lipgloss.JoinVertical(lipgloss.Center, m.spinner.View(), message)
|
|
|
|
|
|
}
|
2026-01-15 17:55:52 +05:00
|
|
|
|
//В зависимости от типа модального окна разные цвета
|
2026-01-14 18:08:43 +05:00
|
|
|
|
var borderColor lipgloss.Color
|
|
|
|
|
|
var modalTitleStyle lipgloss.Style
|
2025-12-29 17:46:48 +05:00
|
|
|
|
switch m.modalType {
|
|
|
|
|
|
case types.ModalError:
|
2026-01-14 18:08:43 +05:00
|
|
|
|
borderColor = ui.ColorError
|
|
|
|
|
|
modalTitleStyle = ui.ModalTitleError
|
2025-12-29 17:46:48 +05:00
|
|
|
|
case types.ModalWarning, types.ModalConfirm:
|
2026-01-14 18:08:43 +05:00
|
|
|
|
borderColor = ui.ColorWarning
|
|
|
|
|
|
modalTitleStyle = ui.ModalTitleWarning
|
2025-12-29 17:46:48 +05:00
|
|
|
|
default:
|
2026-01-14 18:08:43 +05:00
|
|
|
|
borderColor = ui.ColorAccent
|
|
|
|
|
|
modalTitleStyle = ui.ModalTitleDefault
|
2025-12-29 17:46:48 +05:00
|
|
|
|
}
|
2026-01-15 17:55:52 +05:00
|
|
|
|
//Формируем модальный контейнер с контентом внутри и указанным цветом границ.
|
|
|
|
|
|
modalView := ui.ModalContainer.
|
|
|
|
|
|
Width(modalWidth).
|
|
|
|
|
|
Height(modalHeight).
|
|
|
|
|
|
Align(lipgloss.Center).
|
|
|
|
|
|
Border(lipgloss.DoubleBorder()).
|
|
|
|
|
|
BorderForeground(borderColor).
|
|
|
|
|
|
Render(modalBody)
|
|
|
|
|
|
|
2026-01-14 18:08:43 +05:00
|
|
|
|
symbolToRepeat := "*"
|
2026-01-15 17:55:52 +05:00
|
|
|
|
//Если у модального окна есть заголовок, то в modalView записываем новое значение,
|
|
|
|
|
|
//ставя заголовок сверху модального контейнера
|
2026-01-14 18:08:43 +05:00
|
|
|
|
if m.title != "" {
|
|
|
|
|
|
//repeatCount := modalWidth - utf8.RuneCountInString(m.title)
|
|
|
|
|
|
//if repeatCount < 0 {
|
|
|
|
|
|
// repeatCount = 0
|
|
|
|
|
|
//}
|
|
|
|
|
|
//symbolFill := strings.Repeat(symbolToRepeat, repeatCount/2)
|
|
|
|
|
|
//modalView = lipgloss.JoinVertical(lipgloss.Center, modalTitleStyle.Render(symbolFill+m.title+symbolFill), modalView)
|
|
|
|
|
|
modalView = lipgloss.JoinVertical(lipgloss.Center, modalTitleStyle.Render(m.title), modalView)
|
|
|
|
|
|
}
|
|
|
|
|
|
return lipgloss.Place(m.width, m.height,
|
|
|
|
|
|
lipgloss.Center, lipgloss.Center,
|
|
|
|
|
|
modalView,
|
|
|
|
|
|
lipgloss.WithWhitespaceChars(symbolToRepeat),
|
|
|
|
|
|
lipgloss.WithWhitespaceForeground(ui.ColorGrey))
|
2025-12-29 17:46:48 +05:00
|
|
|
|
}
|