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) }