ai-agent/internal/tui/modal.go
admin 8dc496b626
Some checks failed
CI / test (push) Has been cancelled
Release / release (push) Failing after 4m36s
first commit
2026-03-08 15:40:34 +07:00

161 lines
4.2 KiB
Go

package tui
import (
"strings"
"charm.land/lipgloss/v2"
)
type ModalConfig struct {
Title string
Content string
Footer string
Width int
MaxWidth int
BorderStyle lipgloss.Border
PaddingTop int
PaddingBottom int
PaddingLeft int
PaddingRight int
}
func DefaultModalConfig() ModalConfig {
return ModalConfig{
MaxWidth: 60,
BorderStyle: lipgloss.RoundedBorder(),
PaddingTop: 1,
PaddingBottom: 1,
PaddingLeft: 2,
PaddingRight: 2,
}
}
func RenderModal(baseContent string, config ModalConfig, styles Styles, viewportWidth, viewportHeight int) string {
cfg := DefaultModalConfig()
if config.Title != "" {
cfg.Title = config.Title
}
if config.Content != "" {
cfg.Content = config.Content
}
if config.Footer != "" {
cfg.Footer = config.Footer
}
if config.Width > 0 {
cfg.Width = config.Width
}
if config.MaxWidth > 0 {
cfg.MaxWidth = config.MaxWidth
}
if config.BorderStyle != (lipgloss.Border{}) {
cfg.BorderStyle = config.BorderStyle
}
cfg.PaddingTop = config.PaddingTop
cfg.PaddingBottom = config.PaddingBottom
cfg.PaddingLeft = config.PaddingLeft
cfg.PaddingRight = config.PaddingRight
var b strings.Builder
if cfg.Title != "" {
b.WriteString(styles.OverlayTitle.Render(cfg.Title))
b.WriteString("\n")
}
if cfg.Content != "" {
b.WriteString(cfg.Content)
if cfg.Footer != "" {
b.WriteString("\n\n")
}
}
if cfg.Footer != "" {
b.WriteString(styles.OverlayDim.Render(cfg.Footer))
}
contentW := cfg.Width
if contentW == 0 {
lines := strings.Split(b.String(), "\n")
for _, line := range lines {
w := lipgloss.Width(line)
if w+cfg.PaddingLeft+cfg.PaddingRight+2 > contentW {
contentW = w + cfg.PaddingLeft + cfg.PaddingRight + 2
}
}
}
if contentW > cfg.MaxWidth {
contentW = cfg.MaxWidth
}
if contentW < 30 {
contentW = 30
}
if contentW >= viewportWidth-4 {
contentW = viewportWidth - 4
}
box := lipgloss.NewStyle().
Border(cfg.BorderStyle).
BorderForeground(lipgloss.Color(styles.OverlayBorder)).
Padding(cfg.PaddingTop, cfg.PaddingLeft, cfg.PaddingBottom, cfg.PaddingRight).
Width(contentW)
return box.Render(b.String())
}
func CenterOverlay(baseContent, overlay string, viewportWidth, viewportHeight int) string {
baseLines := strings.Split(baseContent, "\n")
overlayLines := strings.Split(overlay, "\n")
startY := (len(baseLines) - len(overlayLines)) / 2
if startY < 0 {
startY = 0
}
for i, ol := range overlayLines {
row := startY + i
if row >= len(baseLines) {
break
}
olW := lipgloss.Width(ol)
padLeft := (viewportWidth - olW) / 2
if padLeft < 0 {
padLeft = 0
}
baseLines[row] = strings.Repeat(" ", padLeft) + ol
}
return strings.Join(baseLines, "\n")
}
type ModalBuilder struct {
config ModalConfig
}
func NewModal() *ModalBuilder {
return &ModalBuilder{config: DefaultModalConfig()}
}
func (mb *ModalBuilder) Title(title string) *ModalBuilder {
mb.config.Title = title
return mb
}
func (mb *ModalBuilder) Content(content string) *ModalBuilder {
mb.config.Content = content
return mb
}
func (mb *ModalBuilder) Footer(footer string) *ModalBuilder {
mb.config.Footer = footer
return mb
}
func (mb *ModalBuilder) Width(width int) *ModalBuilder {
mb.config.Width = width
return mb
}
func (mb *ModalBuilder) MaxWidth(maxWidth int) *ModalBuilder {
mb.config.MaxWidth = maxWidth
return mb
}
func (mb *ModalBuilder) Build(styles Styles, viewportWidth, viewportHeight int) string {
return RenderModal("", mb.config, styles, viewportWidth, viewportHeight)
}
func (mb *ModalBuilder) BuildOnContent(baseContent string, styles Styles, viewportWidth, viewportHeight int) string {
modal := RenderModal("", mb.config, styles, viewportWidth, viewportHeight)
return CenterOverlay(baseContent, modal, viewportWidth, viewportHeight)
}