Работа над отображением объектов, работа со стилями, работа с позиционированиме, изменение таблицы переменных

This commit is contained in:
XeRo 2026-01-14 18:08:43 +05:00
parent ada7db46cf
commit 2e8eee135e
7 changed files with 477 additions and 431 deletions

View file

@ -36,67 +36,19 @@ func (m *FinalModel) Init() tea.Cmd {
return nil return nil
} }
func (m *FinalModel) initViewport(width, height int) {
m.viewport = viewport.New(width, height-8)
m.viewport.Width = width
m.viewport.Height = height - 10
m.ready = true
m.rebuildTable(m.viewport.Width)
m.viewport.SetContent(m.table.Render())
}
func (m *FinalModel) rebuildTable(width int) {
if len(m.variables) == 0 {
m.table = table.New().Headers("Нет переменных")
return
}
availableWidth := width - 12 // примерно: 2 (отступы) + 4 (бордер) + запас
if availableWidth < 70 {
availableWidth = 70
}
t := table.New().
Border(lipgloss.RoundedBorder()).
BorderStyle(ui.TableBorder).
StyleFunc(func(row, col int) lipgloss.Style {
var style lipgloss.Style
switch {
case row == table.HeaderRow:
return ui.TableHeader
case row%2 == 0:
style = ui.TableRowEven.Padding(0, 2)
default:
style = ui.TableRowOdd.Padding(0, 2)
}
switch col {
case 0, 1:
style = style.Width(25)
case 2:
style = style.Width(availableWidth - 50 - 10)
}
return style
}).
Headers("Variable name", "Default value", "Used value")
for _, v := range m.variables {
used := fmt.Sprintf("%v", v.Value)
if used == "" {
used = "<пусто>"
}
t.Row(v.Name, v.Default, used)
}
m.table = t
}
func (m *FinalModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m *FinalModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd var cmd tea.Cmd
switch msg := msg.(type) { switch msg := msg.(type) {
case tea.KeyMsg: case tea.KeyMsg:
switch msg.String() { switch msg.String() {
case "ctrl+c", "q":
return m, tea.Quit
case "up": case "up":
m.viewport.ScrollUp(1) m.viewport.ScrollUp(1)
case "down": case "down":
m.viewport.ScrollDown(1) m.viewport.ScrollDown(1)
case "left":
m.viewport.ScrollLeft(1)
case "right":
m.viewport.ScrollRight(1)
case "pageup": case "pageup":
m.viewport.HalfPageUp() m.viewport.HalfPageUp()
case "pagedown": case "pagedown":
@ -107,15 +59,9 @@ func (m *FinalModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.viewport.GotoBottom() m.viewport.GotoBottom()
} }
case tea.WindowSizeMsg: case tea.WindowSizeMsg:
if !m.ready { m.height = msg.Height
m.width = msg.Width
m.initViewport(msg.Width, msg.Height) m.initViewport(msg.Width, msg.Height)
} else {
m.viewport.Width = msg.Width - 4
m.viewport.Height = msg.Height - 6
}
m.rebuildTable(m.viewport.Width)
rendered := m.table.Render()
m.viewport.SetContent(rendered)
} }
m.viewport, cmd = m.viewport.Update(msg) m.viewport, cmd = m.viewport.Update(msg)
return m, cmd return m, cmd
@ -125,76 +71,103 @@ func (m *FinalModel) View() string {
if !m.ready { if !m.ready {
return "Загрузка таблицы" return "Загрузка таблицы"
} }
containerWidth, containerHeight, contentWidth, _ := ui.ContentSizes(m.width, m.height)
contentWidth := ui.ContentWidth(m.width)
m.rebuildTable(contentWidth) m.rebuildTable(contentWidth)
header := ui.HeaderStyle.Width(contentWidth).Render("Итоговые значения переменных") header := ui.FinalHeader.
footer := ui.FooterStyle.Width(contentWidth).Render(fmt.Sprintf("Строк: %d • q — выход • ↑↓ / jk — скролл", len(m.variables))) Width(contentWidth).
Render("Итоговые значения переменных")
m.viewport.Width = contentWidth - 8 footer := ui.FinalFooter.
m.viewport.Height = m.height - 12 Width(contentWidth).
m.viewport.SetContent(m.table.Render()) Render(fmt.Sprintf("Строк: %d • q — выход • ↑↓ / jk — скролл", len(m.variables)))
renderedTable := m.table.Render()
content := lipgloss.NewStyle().
Align(lipgloss.Center).
Render(renderedTable)
m.viewport.SetContent(content)
body := m.viewport.View() body := m.viewport.View()
fullContent := lipgloss.JoinVertical(lipgloss.Top, header, body, footer)
content := lipgloss.JoinVertical(lipgloss.Left, header, body, footer) container := ui.AppContainer.
container := ui.AppContainer.Width(contentWidth).Render(content) Width(containerWidth).
Height(containerHeight).
return lipgloss.Place(m.width, m.height, Render(fullContent)
return lipgloss.Place(
m.width, m.height,
lipgloss.Center, lipgloss.Center, lipgloss.Center, lipgloss.Center,
container, container,
lipgloss.WithWhitespaceChars("░"), lipgloss.WithWhitespaceChars("+"),
lipgloss.WithWhitespaceForeground(ui.ColorGrey), lipgloss.WithWhitespaceForeground(ui.ColorError),
) )
}
//contentWidth := ui.AppContentWidth(m.width)
//m.viewport.Width = contentWidth - 6 func (m *FinalModel) initViewport(width, height int) {
//m.viewport.Height = m.height - 12
//m.rebuildTable(contentWidth) _, _, contentWidth, contentHeight := ui.ContentSizes(width, height)
//
//header := ui.AppHeaderStyle. tableWidth := contentWidth - 2*1
// Width(contentWidth). tableHeight := int(float64(contentHeight)*ui.TableWithRatio) - 2*1
// Render("Итоговые значения переменных")
// m.viewport = viewport.New(contentWidth, tableHeight)
//footer := ui.AppFooterStyle. //размеры для viewport в нем будет находиться таблица и перемещаться в зависимости от команд
// Width(contentWidth).
// Render(fmt.Sprintf("Строк: %d • q — выход • ↑↓ / jk — скролл", len(m.variables))) m.ready = true
// m.rebuildTable(tableWidth)
//body := m.viewport.View() m.viewport.SetContent(m.table.Render())
//content := lipgloss.JoinVertical(lipgloss.Left, header, body, footer) }
//
//container := ui.AppContainerStyle. func (m *FinalModel) rebuildTable(width int) {
// Width(contentWidth). if len(m.variables) == 0 {
// Render(content) m.table = table.New().
// Border(lipgloss.RoundedBorder()).
//return lipgloss.Place(m.width, m.height, BorderStyle(ui.FinalTableBorder).
// lipgloss.Center, lipgloss.Center, StyleFunc(func(row, col int) lipgloss.Style {
// container, return ui.FinalTableHeader
// lipgloss.WithWhitespaceChars("░"), }).Headers("Нет переменных")
// lipgloss.WithWhitespaceForeground(ui.ColorGrey), return
//) }
variableNameColWidth := 15
//header := lipgloss.NewStyle(). defaultValueColWidth := 25
// Width(m.viewport.Width). usedValueColWidth := width - variableNameColWidth - defaultValueColWidth - 4*1
// Align(lipgloss.Center). //ширина таблицы - ширина первой колонки - ширина второй колонки - 4 границы (2 крайние, 2 между колонками)
// Bold(true). if usedValueColWidth < 30 { //если значение сильно меньше
// Background(ui.ColorPurple). usedValueColWidth = 30
// Foreground(ui.ColorWhite). }
// Padding(1).
// Render("Итоговые значения переменных") t := table.New().
// Width(variableNameColWidth+defaultValueColWidth+usedValueColWidth+4).
//footer := lipgloss.NewStyle(). Border(lipgloss.RoundedBorder()).
// Width(m.viewport.Width). BorderStyle(ui.FinalTableBorder).
// Align(lipgloss.Center). StyleFunc(func(row, col int) lipgloss.Style {
// Foreground(ui.ColorGrey). var style lipgloss.Style
// Padding(0, 1). if row == table.HeaderRow {
// Render(fmt.Sprintf("Строк: %d • q — выход • ↑↓ / jk — скролл", len(m.variables))) return ui.FinalTableHeader
// }
//body := m.viewport.View() if row%2 == 0 {
//return lipgloss.JoinVertical(lipgloss.Center, style = ui.FinalTableEvenRow
// header, } else {
// body, style = ui.FinalTableOddRow
// footer, }
//) switch col {
case 0:
return style.Width(variableNameColWidth).Align(lipgloss.Left)
case 1:
return style.Width(defaultValueColWidth).Align(lipgloss.Left)
case 2:
return style.Width(usedValueColWidth).Align(lipgloss.Left)
}
return style
}).
Headers("Variable name", "Default value", "Used value")
for _, v := range m.variables {
used := fmt.Sprintf("%v", v.Value)
if used == "" {
used = "<Пусто>"
}
t.Row(v.Name, v.Default, used)
}
m.table = t
} }

View file

@ -6,11 +6,11 @@ import (
"bubble/types" "bubble/types"
"fmt" "fmt"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/glamour"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
"github.com/muesli/reflow/wordwrap" "github.com/muesli/reflow/wordwrap"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"strings" "strings"
"unicode/utf8"
) )
type FormModel struct { type FormModel struct {
@ -77,12 +77,7 @@ func NewFormModel(application *types.PackageApplication, sectionIdx, focus, widt
height: height, height: height,
} }
if currentSection.Type == types.SectionAbout { if currentSection.Type == types.SectionAbout {
md := strings.Join(currentSection.Paragraphs, "\n\n") m.renderedContent = simpleAboutRender(currentSection.Paragraphs, width)
renderer, _ := glamour.NewTermRenderer(
glamour.WithAutoStyle(),
glamour.WithWordWrap(width-10))
rendered, _ := renderer.Render(md)
m.renderedContent = rendered
m.focus = 0 m.focus = 0
} }
return m return m
@ -93,24 +88,17 @@ func (m *FormModel) Init() tea.Cmd {
} }
func (m *FormModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m *FormModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) { switch msg := msg.(type) {
case tea.WindowSizeMsg: case tea.WindowSizeMsg:
m.width = msg.Width m.width = msg.Width
m.height = msg.Height m.height = msg.Height
if m.CurrentSection.Type == types.SectionAbout { if m.CurrentSection.Type == types.SectionAbout {
md := strings.Join(m.CurrentSection.Paragraphs, "\n\n") m.renderedContent = simpleAboutRender(m.CurrentSection.Paragraphs, m.width)
renderer, _ := glamour.NewTermRenderer(
glamour.WithAutoStyle(),
glamour.WithWordWrap(m.width-10),
)
rendered, _ := renderer.Render(md)
m.renderedContent = rendered
} }
return m, nil return m, nil
case tea.KeyMsg: case tea.KeyMsg:
switch msg.String() { switch msg.String() {
case "ctrl+c", "q":
return m, tea.Quit
case "esc": case "esc":
for _, field := range m.FieldModels { for _, field := range m.FieldModels {
if field.CaptureKeys() { if field.CaptureKeys() {
@ -134,7 +122,6 @@ func (m *FormModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}, },
} }
} }
//вернуться к списку приложений, чтобы выбрать другое приложение.
case "enter": case "enter":
logrus.Infof("sections %s, focus %d, fields len %d", m.CurrentSection.Name, m.focus, len(m.FieldModels)) logrus.Infof("sections %s, focus %d, fields len %d", m.CurrentSection.Name, m.focus, len(m.FieldModels))
if len(m.FieldModels) == m.focus { if len(m.FieldModels) == m.focus {
@ -176,71 +163,45 @@ func (m *FormModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case types.SelectedOptionMsg: case types.SelectedOptionMsg:
logrus.Infof("chose %+v", msg) logrus.Infof("chose %+v", msg)
} }
cmd := m.updateFields(msg) return m, m.updateFields(msg)
return m, cmd
} }
func (m *FormModel) View() string { func (m *FormModel) View() string {
contentWidth := ui.ContentWidth(m.width)
var res strings.Builder containerWidth, containerHeight, contentWidth, _ := ui.ContentSizes(m.width, m.height)
formWidth := contentWidth - 2*1
var strokes []string
for i, field := range m.FieldModels { for i, field := range m.FieldModels {
fieldStr := wordwrap.String(field.View(), contentWidth-10) fieldStroke := wordwrap.String(field.View(), formWidth)
if i == m.focus { if i == m.focus {
res.WriteString(ui.FormFieldFocused.Render(fieldStr)) strokes = append(strokes, ui.FormFieldFocused.Render(fieldStroke))
} else { } else {
res.WriteString(ui.FormFieldUnfocused.Render(fieldStr)) strokes = append(strokes, ui.FormFieldUnfocused.Render(fieldStroke))
} }
res.WriteString("\n\n")
} }
if m.CurrentSection.Type == types.SectionAbout { if m.CurrentSection.Type == types.SectionAbout {
res.WriteString(m.renderedContent) strokes = append([]string{}, m.renderedContent)
} }
nextBtn := "[ next section ]" nextBtn := "[ next section ]"
if m.focus == len(m.FieldModels) { if m.focus == len(m.FieldModels) {
res.WriteString(ui.FormNextButton.Render(nextBtn)) strokes = append(strokes, ui.FormNextButton.Render(nextBtn))
} else { } else {
res.WriteString(ui.FormFieldUnfocused.Render(nextBtn)) strokes = append(strokes, ui.FormFieldUnfocused.Render(nextBtn))
} }
container := ui.FormContainer.Width(contentWidth).Render(res.String()) container := ui.AppContainer.
return lipgloss.Place(m.width, m.height, Width(containerWidth).
Height(containerHeight).
Render(strings.Join(strokes, "\n\n"))
return lipgloss.Place(
m.width, m.height,
lipgloss.Center, lipgloss.Center, lipgloss.Center, lipgloss.Center,
container, container,
lipgloss.WithWhitespaceChars(" "), lipgloss.WithWhitespaceChars(" "),
) )
//for i, field := range m.FieldModels {
// fieldStr := wordwrap.String(field.View(), contentWidth-8)
// if i == m.focus {
// res.WriteString(ui.FormFocusedStyle.Render(fieldStr))
// } else {
// res.WriteString(ui.FormUnfocusedStyle.Render(fieldStr))
// }
// res.WriteString("\n\n")
//}
//
//if m.CurrentSection.Type == types.SectionAbout {
// res.WriteString("\n\n" + m.renderedContent)
//}
//
//button := "[ next section ]"
//if m.focus == len(m.FieldModels) {
// res.WriteString(ui.FormFocusedStyle.Render(button))
//} else {
// res.WriteString(ui.FormUnfocusedStyle.Render(button))
//}
//
//container := ui.FormStyle.Width(contentWidth).Render(res.String())
//
//return lipgloss.Place(m.width, m.height,
// lipgloss.Center, lipgloss.Top,
// container,
// lipgloss.WithWhitespaceChars(" "),
//)
} }
func (m *FormModel) updateFields(msg tea.Msg) tea.Cmd { func (m *FormModel) updateFields(msg tea.Msg) tea.Cmd {
@ -283,3 +244,42 @@ func (m *FormModel) updateFields(msg tea.Msg) tea.Cmd {
} }
return tea.Batch(cmds...) return tea.Batch(cmds...)
} }
func simpleAboutRender(paragraphs []string, width int) string {
var sb strings.Builder
for _, p := range paragraphs {
p = strings.TrimSpace(p)
if p == "" {
sb.WriteString("\n")
continue
}
switch {
case strings.HasPrefix(p, "# "):
// Заголовок 1 уровня
h1Len := utf8.RuneCountInString(p[2:])
h1Offset := strings.Repeat(" ", (width-h1Len)/2)
sb.WriteString(ui.AboutTitle.Render(h1Offset+p[2:]) + "\n\n")
case strings.HasPrefix(p, "## "):
// Заголовок 2 уровня
sb.WriteString(ui.AboutH2.Render(p[3:]) + "\n\n")
case strings.HasPrefix(p, "### "):
// Заголовок 3 уровня
sb.WriteString(" " + ui.AboutH3.Render(p[4:]) + "\n\n")
case strings.HasPrefix(p, "- "), strings.HasPrefix(p, "* "):
// Список простой
text := strings.TrimLeft(p[2:], " ")
wrapped := wordwrap.String(text, width-10)
sb.WriteString(" " + ui.AboutBullet.Render("• ") + ui.AboutNormal.Render(wrapped) + "\n")
case strings.HasPrefix(p, "> "):
// Цитаты
text := strings.TrimPrefix(p, "> ")
wrapped := wordwrap.String(text, width-12)
sb.WriteString(" " + ui.AboutNormal.Foreground(ui.ColorGrey).Render("│ "+wrapped) + "\n")
default:
// Обычный текст
wrapped := wordwrap.String(p, width-8)
sb.WriteString(" " + ui.AboutNormal.Render(wrapped) + "\n\n")
}
}
return sb.String()
}

View file

@ -7,6 +7,7 @@ import (
"github.com/charmbracelet/bubbles/list" "github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
"github.com/sirupsen/logrus"
) )
type ListApplicationItem struct { type ListApplicationItem struct {
@ -42,9 +43,9 @@ func NewListApplicationsModel(applications []types.PackageApplication, width, he
delegate.Styles.SelectedDesc = ui.ListSelectedStyle delegate.Styles.SelectedDesc = ui.ListSelectedStyle
delegate.ShowDescription = false delegate.ShowDescription = false
l := list.New(items, delegate, width, height) l := list.New(items, delegate, width, height)
l.Title = "Выберите приложение для настройки" l.Title = "Приложения"
l.Styles.Title = ui.ListTitleStyle l.Styles.Title = ui.ListTitleStyle
l.SetShowHelp(false)
return &ListApplicationsModel{ return &ListApplicationsModel{
List: l, List: l,
width: width, width: width,
@ -65,15 +66,13 @@ func (m *ListApplicationsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil return m, nil
case tea.KeyMsg: case tea.KeyMsg:
switch msg.String() { switch msg.String() {
case "ctrl+c", "q":
return m, tea.Quit
case "enter": case "enter":
selected := m.List.SelectedItem().(ListApplicationItem) selected := m.List.SelectedItem().(ListApplicationItem)
return m, func() tea.Msg { return m, func() tea.Msg {
return types.ShowModalMsg{ return types.ShowModalMsg{
Type: types.ModalConfirm, Type: types.ModalConfirm,
Title: "Начать установку", Title: "Начать установку",
Message: fmt.Sprintf("Вы хотит настроить приложение:\n\n%s\n\nПродолжить?", selected.Title()), Message: fmt.Sprintf("Вы хотите настроить приложение:\n\n%s\n\nПродолжить?", selected.Title()),
ModalButtons: []types.ModalButton{ ModalButtons: []types.ModalButton{
{Text: "Да", Value: true}, {Text: "Да", Value: true},
{Text: "Нет", Value: false}, {Text: "Нет", Value: false},
@ -95,46 +94,63 @@ func (m *ListApplicationsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} }
func (m *ListApplicationsModel) View() string { func (m *ListApplicationsModel) View() string {
contentWidth := ui.ContentWidth(m.width) gapStroke := " "
listWidth := int(float64(contentWidth) * ui.ListWithRatio) containerWidth, containerHeight, contentWidth, contentHeight := ui.ContentSizes(m.width, m.height)
m.List.SetSize(listWidth-6, m.height-10)
listView := m.List.View() listWidth := int(float64(contentWidth)*ui.ListWithRatio) - 2*1
//ширина листа = пропорция от ширины контента - границы для контейнера листа
listHeight := contentHeight - 2*1
//высота листа = высота контента - границы для контейнера листа
descWidth := int(float64(contentWidth)*(1-ui.ListWithRatio)) - 2*1 - len(gapStroke)
//ширина описания = обратная пропорция листа от ширины контента - границы для контейнера описания - отступ от контейнера листа
descHeight := contentHeight - 2*1
//высота листа = высота контента - границы для контейнера описания
m.List.SetSize(listWidth, listHeight)
//logs.go logrus.DebugLevel set
logrus.Debugf(
"window: width %d, height %d\ncontainer: width %d, height %d\ncontent: width %d, height %d\nlist: width %d, height %d\ndesc: width %d, height %d\n\n",
m.width, m.height,
containerWidth, containerHeight,
contentWidth, contentHeight,
listWidth, listHeight,
descWidth, descHeight,
)
listView := lipgloss.NewStyle().
Width(listWidth).
Height(listHeight).
Border(lipgloss.RoundedBorder()).
BorderForeground(ui.ColorPurple).
AlignVertical(lipgloss.Top).
Render(m.List.View())
var desc string var desc string
if sel := m.List.SelectedItem(); sel != nil { if sel := m.List.SelectedItem(); sel != nil {
desc = sel.(ListApplicationItem).Description() desc = sel.(ListApplicationItem).Description()
} }
descView := ui.ListDescriptionStyle.Width(contentWidth - listWidth - 10).Render(desc)
body := lipgloss.JoinHorizontal(lipgloss.Top, listView, " ", descView)
container := ui.ListContainer.Width(contentWidth).Render(body)
return lipgloss.Place(m.width, m.height, descView := ui.ListDescriptionStyle.
Width(descWidth).
Height(descHeight).
Border(lipgloss.RoundedBorder()).
BorderForeground(ui.ColorError).
AlignVertical(lipgloss.Top).
Render(
desc,
)
body := lipgloss.JoinHorizontal(lipgloss.Center, listView, gapStroke, descView)
container := ui.AppContainer.
Width(containerWidth).
Height(containerHeight).
Render(body)
return lipgloss.Place(
m.width, m.height,
lipgloss.Center, lipgloss.Center, lipgloss.Center, lipgloss.Center,
container, container,
lipgloss.WithWhitespaceChars("░"), lipgloss.WithWhitespaceChars("░"),
lipgloss.WithWhitespaceForeground(ui.ColorGrey), lipgloss.WithWhitespaceForeground(ui.ColorWarning),
) )
//var desc string
//if selected := m.List.SelectedItem(); selected != nil {
// desc = selected.(ListApplicationItem).Description()
//}
//descView := ui.ListDescriptionStyle.
// Width(contentWidth/2 - 4).
// Render(desc)
//
//body := lipgloss.JoinHorizontal(lipgloss.Top, listView, " ", descView)
//
//container := ui.ListStyle.
// Width(contentWidth).
// Render(body)
//
//return lipgloss.Place(m.width, m.height,
// lipgloss.Center, lipgloss.Center,
// container,
// lipgloss.WithWhitespaceChars("░"),
// lipgloss.WithWhitespaceForeground(ui.ColorGrey),
//)
} }

View file

@ -67,8 +67,24 @@ func (m *Main) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} }
case tea.KeyMsg: case tea.KeyMsg:
switch msg.String() { switch msg.String() {
case "ctrl+c", "q": case "ctrl+c":
return m, tea.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: case types.NextSectionMsg:
m.CurrentSectionIdx++ m.CurrentSectionIdx++
@ -103,14 +119,12 @@ func (m *Main) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.current = NewFinalModel(m.CurrentApplication.Variables, m.Width, m.Height) m.current = NewFinalModel(m.CurrentApplication.Variables, m.Width, m.Height)
} else { } else {
m.current = NewFormModel(m.CurrentApplication, m.CurrentSectionIdx, 0, m.Width, m.Height) m.current = NewFormModel(m.CurrentApplication, m.CurrentSectionIdx, 0, m.Width, m.Height)
} }
case types.SugarMsg: case types.SugarMsg:
m.Sugar(msg) m.Sugar(msg)
case types.SelectedAppMsg: case types.SelectedAppMsg:
//получаем сообщение о выборе приложения //получаем сообщение о выборе приложения
m.CurrentApplication = &msg.App m.CurrentApplication = &msg.App
//ищем доступную секцию //ищем доступную секцию
for sectionIdx := 0; sectionIdx < len(m.CurrentApplication.Sections); sectionIdx++ { for sectionIdx := 0; sectionIdx < len(m.CurrentApplication.Sections); sectionIdx++ {
section := m.CurrentApplication.Sections[sectionIdx] section := m.CurrentApplication.Sections[sectionIdx]
@ -136,7 +150,6 @@ func (m *Main) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.overlay = NewModalModel(msg, m.Width, m.Height) m.overlay = NewModalModel(msg, m.Width, m.Height)
return m, nil return m, nil
case types.CloseModalMsg: case types.CloseModalMsg:
logrus.Infof("modal close msg get")
m.overlay = nil m.overlay = nil
return m, tea.Batch(cmds...) return m, tea.Batch(cmds...)
} }

View file

@ -73,113 +73,89 @@ func (m *ModalModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} }
func (m *ModalModel) View() string { func (m *ModalModel) View() string {
contentWidth := ui.ContentWidth(m.width)
modalWidth := min(contentWidth-10, ui.ModalMaxHeight)
if modalWidth < 60 {
modalWidth = 60
}
content := lipgloss.NewStyle(). containerWidth, containerHeight, _, _ := ui.ContentSizes(m.width, m.height)
Width(modalWidth-8).
Padding(1, 3).
Render(m.message)
modalWidth := min(containerWidth, ui.ModalMaxWidth)
modalHeight := min(containerHeight, ui.ModalMaxHeight)
message := lipgloss.NewStyle().Render(m.message)
var buttons []string var buttons []string
for i, button := range m.buttons {
for i, btn := range m.buttons {
style := ui.ModalButtonNormal style := ui.ModalButtonNormal
if i == m.focus { if i == m.focus {
style = ui.ModalButtonFocused style = ui.ModalButtonFocused
} }
buttons = append(buttons, style.Render(btn.Text)) buttons = append(buttons, style.Render(button.Text))
} }
buttonRow := lipgloss.JoinHorizontal(lipgloss.Center, buttons...) buttonRow := lipgloss.JoinHorizontal(lipgloss.Center, buttons...)
modalBody := lipgloss.JoinVertical(lipgloss.Center, message, "", buttonRow)
body := lipgloss.JoinVertical(lipgloss.Center, content, "\n\n", buttonRow) var borderColor lipgloss.Color
var modalTitleStyle lipgloss.Style
// Бордер и цвет в зависимости от типа
var modalStyle lipgloss.Style
var titleStyle lipgloss.Style
switch m.modalType { switch m.modalType {
case types.ModalError: case types.ModalError:
modalStyle = ui.ModalBorderError borderColor = ui.ColorError
titleStyle = ui.ModalTitleError modalTitleStyle = ui.ModalTitleError
case types.ModalWarning, types.ModalConfirm: case types.ModalWarning, types.ModalConfirm:
modalStyle = ui.ModalBorderWarning borderColor = ui.ColorWarning
titleStyle = ui.ModalTitleWarning modalTitleStyle = ui.ModalTitleWarning
default: default:
modalStyle = ui.ModalBorderInfo borderColor = ui.ColorAccent
titleStyle = ui.ModalTitleDefault modalTitleStyle = ui.ModalTitleDefault
} }
modal := lipgloss.NewStyle().
Width(modalWidth).
Padding(2, 4).
Border(lipgloss.DoubleBorder()).
BorderForeground(modalStyle.GetBorderBottomForeground()).
Render(body)
if m.title != "" {
title := titleStyle.Width(modalWidth).Align(lipgloss.Center).Render(m.title)
modal = lipgloss.JoinVertical(lipgloss.Center, title, modal)
}
return lipgloss.Place(m.width, m.height,
lipgloss.Center, lipgloss.Center,
modal,
lipgloss.WithWhitespaceChars("."),
lipgloss.WithWhitespaceForeground(ui.ColorGrey))
//modalWidth := min(m.width-20, ui.ModalMaxHeight)
//if modalWidth < 60 {
// modalWidth = 60
//}
//var borderColor lipgloss.Color
//var titleStyle lipgloss.Style
// //
//switch m.modalType { //switch m.modalType {
//case types.ModalError: //case types.ModalError:
// borderColor = ui.ColorRed // modalStyle = ui.ModalBorderError
// titleStyle = lipgloss.NewStyle().Foreground(ui.ColorRed).Bold(true) // modalTitleStyle = ui.ModalTitleError
//case types.ModalWarning, types.ModalConfirm: //case types.ModalWarning, types.ModalConfirm:
// borderColor = ui.ColorPurple // modalStyle = ui.ModalBorderWarning
// titleStyle = lipgloss.NewStyle().Foreground(ui.ColorPurple).Bold(true) // modalTitleStyle = ui.ModalTitleWarning
//default: //default:
// borderColor = ui.ColorBlue // modalStyle = ui.ModalBorderInfo
// titleStyle = lipgloss.NewStyle().Foreground(ui.ColorBlue).Bold(true) // modalTitleStyle = ui.ModalTitleDefault
//} //}
//content := lipgloss.NewStyle().Width(m.width-10).Padding(1, 2).Render(m.message) var modalView string
//var buttons []string symbolToRepeat := "*"
//for i, btn := range m.buttons { if m.title != "" {
// style := lipgloss.NewStyle(). modalView = ui.ModalContainer.
// Padding(0, 2). Width(modalWidth).
// Border(lipgloss.RoundedBorder()). Height(modalHeight).
// BorderForeground(ui.ColorGrey) Align(lipgloss.Center, lipgloss.Center).
// if i == m.focus { Border(lipgloss.DoubleBorder()).
// style = style.Foreground(ui.ColorRed).BorderForeground(ui.ColorRed).Bold(true) BorderForeground(borderColor).
// } else { Render(modalBody)
// style = style.Foreground(ui.ColorGreenCyan) //modalView = ui.ModalContainer.
// }
// buttons = append(buttons, style.Render(btn.Text))
//}
//buttonShow := lipgloss.JoinHorizontal(lipgloss.Center, buttons...)
//body := lipgloss.JoinVertical(lipgloss.Center, content, "\n", buttonShow)
//
//modal := lipgloss.NewStyle().
// Width(modalWidth). // Width(modalWidth).
// Height(modalHeight).
// BorderForeground(borderColor).
// Render(modalBody)
//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)
} else {
modalView = ui.ModalContainer.
Width(modalWidth).
Height(modalHeight).
BorderStyle(lipgloss.ASCIIBorder()).
BorderForeground(borderColor).
Render(modalBody)
//modalView = modalStyle.
// Width(modalWidth).
// Height(modalHeight).
// Padding(1, 3). // Padding(1, 3).
// Border(lipgloss.DoubleBorder()). // Border(lipgloss.DoubleBorder()).
// BorderForeground(borderColor). // Render(modalBody)
// Render(body) }
// return lipgloss.Place(m.width, m.height,
//if m.title != "" { lipgloss.Center, lipgloss.Center,
// title := titleStyle.Width(modalWidth - 6).Render(m.title) modalView,
// modal = lipgloss.JoinVertical(lipgloss.Center, title, modal) lipgloss.WithWhitespaceChars(symbolToRepeat),
//} lipgloss.WithWhitespaceForeground(ui.ColorGrey))
//
//return lipgloss.Place(m.width, m.height,
// lipgloss.Center, lipgloss.Center,
// modal,
// lipgloss.WithWhitespaceChars("."),
// lipgloss.WithWhitespaceForeground(ui.ColorPurple),
//)
} }

View file

@ -1,27 +1,63 @@
package ui package ui
import "github.com/charmbracelet/lipgloss" import (
"github.com/charmbracelet/lipgloss"
const (
AppMaxWidth = 110
AppMinWidth = 80
AppSidePadding = 4
ModalMaxWidth = 80
ModalMaxHeight = 30
ListWithRatio = 0.4
) )
func AppContentWidth(screenWidth int) int { const (
w := screenWidth - 2*AppSidePadding ApplicationMinWidth = 60
if w > AppMaxWidth { ApplicationMaxHeight = 40
return AppMaxWidth ApplicationMinHeight = 20
ApplicationHorizontalOffset = 3
ApplicationVerticalOffset = 1
ApplicationContainerPadding = 4
ModalMaxWidth = 50
ModalMaxHeight = 15
ListWithRatio = 0.4
TableWithRatio = 0.8
)
// ContentSizes вычисляет размеры для контейнера внутри окна и контента внутри контейнера.
//
// Принимает 2 параметра типа int, screenWidth и screenHeight ширина и высота окна.
//
// Возвращает 4 значения типа int.
//
// containerWidth это ширина контейнера внутри окна с отступами от левого и правого краёв, и с учетом границ
//
// containerHeight это высота контейнера внутри окна с отступами от верхнего и нижнего краёв, и с учетом границ
//
// contentWidth это ширина контента внутри контейнера с отступами от левого и правого краёв контейнера
//
// contentHeight это высота контента внутри контейнера с отступами от верхнего и нижнего краёв контейнера
func ContentSizes(screenWidth, screenHeight int) (containerWidth, containerHeight, contentWidth, contentHeight int) {
containerWidth = screenWidth - 2*ApplicationHorizontalOffset
if containerWidth < ApplicationMinWidth {
//если ширина слишком маленькая ставим минимальную поддерживаемую
containerWidth = ApplicationMinWidth
} }
if w < AppMinWidth { containerWidth = containerWidth - 2*1
return AppMinWidth //Вычитаем из ширины контейнера 2 условных единицы для границ
//Пример, окно 70, контейнер будет 64, места для контента тогда 62
containerHeight = screenHeight - 2*ApplicationVerticalOffset
if containerHeight < ApplicationMinHeight {
//если высота слишком маленькая ставим минимально поддерживаемую
containerHeight = ApplicationMinHeight
} }
return w containerHeight = containerHeight - 2*1
//Вычитаем из высоты контейнера 2 условных единицы для границ
//Пример, окно 30, контейнер будет 28, места для контента тогда 26
contentWidth = containerWidth - 2*ApplicationHorizontalOffset
//Вычитаем из ширины контейнера отступы для контента внутри
contentHeight = containerHeight - 2*ApplicationVerticalOffset
//Вычитаем из высоты контейнера отступы для контента внутри
return
} }
// Используемые цвета
var ( var (
ColorFg = lipgloss.Color("#eeeeee") ColorFg = lipgloss.Color("#eeeeee")
ColorBg = lipgloss.Color("#0d1117") ColorBg = lipgloss.Color("#0d1117")
@ -32,34 +68,93 @@ var (
ColorPurple = lipgloss.Color("#cba6f7") ColorPurple = lipgloss.Color("#cba6f7")
ColorGrey = lipgloss.Color("#585b70") ColorGrey = lipgloss.Color("#585b70")
ColorDarkGrey = lipgloss.Color("#313244") ColorDarkGrey = lipgloss.Color("#313244")
)
AppContainer = lipgloss.NewStyle(). var AppContainer = lipgloss.NewStyle().
Padding(2, 4). Padding(ApplicationVerticalOffset, ApplicationHorizontalOffset).
Border(lipgloss.RoundedBorder()).
BorderForeground(ColorPurple)
HeaderStyle = lipgloss.NewStyle().
Bold(true).
Foreground(ColorPurple).
Background(ColorDarkGrey).
Padding(1, 2).
MarginBottom(1).
Align(lipgloss.Center)
FooterStyle = lipgloss.NewStyle().
Foreground(ColorGrey).
Padding(0, 2).
MarginTop(1).
Align(lipgloss.Center)
ListContainer = lipgloss.NewStyle().
Padding(1, 3).
Border(lipgloss.RoundedBorder()). Border(lipgloss.RoundedBorder()).
BorderForeground(ColorAccent) BorderForeground(ColorAccent)
// Стили для списка приложений
var (
ListTitleStyle = lipgloss.NewStyle().Foreground(ColorPurple).Italic(true).Bold(true) ListTitleStyle = lipgloss.NewStyle().Foreground(ColorPurple).Italic(true).Bold(true)
ListSelectedStyle = lipgloss.NewStyle().Foreground(ColorError).Bold(true) ListSelectedStyle = lipgloss.NewStyle().Foreground(ColorError).Bold(true)
ListDescriptionStyle = lipgloss.NewStyle().Foreground(ColorAccent2).MarginTop(1) ListDescriptionStyle = lipgloss.NewStyle().Foreground(ColorAccent2)
)
// Стили для about
var (
AboutTitle = lipgloss.NewStyle().
Foreground(ColorAccent).
Bold(true).
AlignHorizontal(lipgloss.Center)
AboutH2 = lipgloss.NewStyle().
Foreground(ColorAccent).
Bold(true)
AboutH3 = lipgloss.NewStyle().
Foreground(ColorAccent).
Bold(true)
AboutBullet = lipgloss.NewStyle().
Foreground(ColorGrey)
AboutNormal = lipgloss.NewStyle().
Foreground(ColorFg)
)
// Стили для форм и полей ввода
var (
FormFieldFocused = lipgloss.NewStyle().Foreground(ColorError).Bold(true)
FormFieldUnfocused = lipgloss.NewStyle().Foreground(ColorAccent2)
FormNextButton = lipgloss.NewStyle().Foreground(ColorPurple).Bold(true)
SelectFileDirDeniedStyle = lipgloss.NewStyle().Foreground(ColorGrey)
SelectFileDirParentStyle = lipgloss.NewStyle().Foreground(ColorPurple)
SelectFileStyle = lipgloss.NewStyle().Foreground(ColorAccent)
SelectDirStyle = lipgloss.NewStyle().Foreground(ColorAccent2)
)
// Стили для таблицы переменных Final
var (
FinalHeader = lipgloss.NewStyle().
Background(ColorDarkGrey).
Foreground(ColorPurple).
Bold(true).
Align(lipgloss.Center).
MarginBottom(1).
Padding(1, 2)
FinalFooter = lipgloss.NewStyle().
Foreground(ColorGrey).
MarginTop(1).
Padding(1, 0).
Align(lipgloss.Center)
FinalTableBorder = lipgloss.NewStyle().
Foreground(ColorPurple)
FinalTableHeader = lipgloss.NewStyle().
Foreground(ColorPurple).
Bold(true).
Align(lipgloss.Center).
Padding(0, 1)
FinalTableEvenRow = lipgloss.NewStyle().
Background(ColorDarkGrey).
Padding(0, 2)
FinalTableOddRow = lipgloss.NewStyle().
Padding(0, 2)
)
// Стили для модальных окон
var (
ModalContainer = lipgloss.NewStyle().
Border(lipgloss.DoubleBorder()).
Align(lipgloss.Center, lipgloss.Center)
//Padding(ApplicationVerticalOffset, ApplicationHorizontalOffset)
ModalBorderError = lipgloss.NewStyle().BorderForeground(ColorError) ModalBorderError = lipgloss.NewStyle().BorderForeground(ColorError)
ModalBorderWarning = lipgloss.NewStyle().BorderForeground(ColorWarning) ModalBorderWarning = lipgloss.NewStyle().BorderForeground(ColorWarning)
@ -82,22 +177,10 @@ var (
BorderForeground(ColorError). BorderForeground(ColorError).
Foreground(ColorError). Foreground(ColorError).
Bold(true) Bold(true)
)
FormContainer = lipgloss.NewStyle(). //Старые стили
Padding(2, 4). //var (
Border(lipgloss.RoundedBorder()).
BorderForeground(ColorAccent)
FormFieldFocused = lipgloss.NewStyle().Foreground(ColorError).Bold(true)
FormFieldUnfocused = lipgloss.NewStyle().Foreground(ColorAccent2)
FormNextButton = lipgloss.NewStyle().Foreground(ColorPurple).Bold(true)
TableBorder = lipgloss.NewStyle().Foreground(ColorPurple)
TableHeader = lipgloss.NewStyle().Foreground(ColorPurple).Bold(true).Align(lipgloss.Center)
TableRowEven = lipgloss.NewStyle().Foreground(ColorFg).Background(ColorDarkGrey)
TableRowOdd = lipgloss.NewStyle().Foreground(ColorFg)
TableCell = lipgloss.NewStyle().Padding(0, 2)
//ColorWhite = lipgloss.Color("#ffffff") //ColorWhite = lipgloss.Color("#ffffff")
//ColorGrey = lipgloss.Color("#626262") //ColorGrey = lipgloss.Color("#626262")
//ColorBlack = lipgloss.Color("#000000") //ColorBlack = lipgloss.Color("#000000")
@ -131,10 +214,6 @@ var (
//TableEvenRowStyle = TableCellStyle.Foreground(ColorWhite) //TableEvenRowStyle = TableCellStyle.Foreground(ColorWhite)
//TableBorderStyle = lipgloss.NewStyle().Foreground(ColorPurple) //TableBorderStyle = lipgloss.NewStyle().Foreground(ColorPurple)
// //
SelectFileDirDeniedStyle = lipgloss.NewStyle().Foreground(ColorGrey)
SelectFileDirParentStyle = lipgloss.NewStyle().Foreground(ColorPurple)
SelectFileStyle = lipgloss.NewStyle().Foreground(ColorAccent)
SelectDirStyle = lipgloss.NewStyle().Foreground(ColorAccent2)
// //
//SelectStyle = lipgloss.NewStyle().Foreground(ColorGreenCyan) //SelectStyle = lipgloss.NewStyle().Foreground(ColorGreenCyan)
//InputStyle = lipgloss.NewStyle().Foreground(ColorGreenCyan) //InputStyle = lipgloss.NewStyle().Foreground(ColorGreenCyan)
@ -151,15 +230,4 @@ var (
//ListSelectedItemStyle = lipgloss.NewStyle().Foreground(ColorRed).Bold(true) //ListSelectedItemStyle = lipgloss.NewStyle().Foreground(ColorRed).Bold(true)
//ListTitleStyle = lipgloss.NewStyle().Foreground(ColorPurple).Italic(true) //ListTitleStyle = lipgloss.NewStyle().Foreground(ColorPurple).Italic(true)
//ListDescriptionStyle = lipgloss.NewStyle().Foreground(ColorGreenCyan).MarginTop(1) //ListDescriptionStyle = lipgloss.NewStyle().Foreground(ColorGreenCyan).MarginTop(1)
) //)
func ContentWidth(screenWidth int) int {
w := screenWidth - 2*AppSidePadding
if w > AppMaxWidth {
return AppMaxWidth
}
if w < AppMinWidth {
return AppMinWidth
}
return w
}

View file

@ -1,4 +1,4 @@
package types package types
const SectionAbout = "abouta" const SectionAbout = "about"
const SectionFinal = "final" const SectionFinal = "final"