opencode/packages/tui/internal/components/modal/modal.go
2025-07-02 16:08:11 -05:00

145 lines
3.2 KiB
Go

package modal
import (
"strings"
"github.com/charmbracelet/lipgloss/v2"
"github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
)
// CloseModalMsg is a message to signal that the active modal should be closed.
type CloseModalMsg struct{}
// Modal is a reusable modal component that handles frame rendering and overlay placement
type Modal struct {
width int
height int
title string
maxWidth int
maxHeight int
fitContent bool
}
// ModalOption is a function that configures a Modal
type ModalOption func(*Modal)
// WithTitle sets the modal title
func WithTitle(title string) ModalOption {
return func(m *Modal) {
m.title = title
}
}
// WithMaxWidth sets the maximum width
func WithMaxWidth(width int) ModalOption {
return func(m *Modal) {
m.maxWidth = width
m.fitContent = false
}
}
// WithMaxHeight sets the maximum height
func WithMaxHeight(height int) ModalOption {
return func(m *Modal) {
m.maxHeight = height
}
}
func WithFitContent(fit bool) ModalOption {
return func(m *Modal) {
m.fitContent = fit
}
}
// New creates a new Modal with the given options
func New(opts ...ModalOption) *Modal {
m := &Modal{
maxWidth: 0,
maxHeight: 0,
fitContent: true,
}
for _, opt := range opts {
opt(m)
}
return m
}
func (m *Modal) SetTitle(title string) {
m.title = title
}
// Render renders the modal centered on the screen
func (m *Modal) Render(contentView string, background string) string {
t := theme.CurrentTheme()
outerWidth := layout.Current.Container.Width - 8
if m.maxWidth > 0 && outerWidth > m.maxWidth {
outerWidth = m.maxWidth
}
if m.fitContent {
titleWidth := lipgloss.Width(m.title)
contentWidth := lipgloss.Width(contentView)
largestWidth := max(titleWidth+2, contentWidth)
outerWidth = largestWidth + 6
}
innerWidth := outerWidth - 4
baseStyle := styles.NewStyle().Foreground(t.TextMuted()).Background(t.BackgroundElement())
var finalContent string
if m.title != "" {
titleStyle := baseStyle.
Foreground(t.Primary()).
Bold(true).
Padding(0, 1)
escStyle := baseStyle.Foreground(t.TextMuted())
escText := escStyle.Render("esc")
// Calculate position for esc text
titleWidth := lipgloss.Width(m.title)
escWidth := lipgloss.Width(escText)
spacesNeeded := max(0, innerWidth-titleWidth-escWidth-2)
spacer := strings.Repeat(" ", spacesNeeded)
titleLine := m.title + spacer + escText
titleLine = titleStyle.Render(titleLine)
finalContent = strings.Join([]string{titleLine, "", contentView}, "\n")
} else {
finalContent = contentView
}
modalStyle := baseStyle.
PaddingTop(1).
PaddingBottom(1).
PaddingLeft(2).
PaddingRight(2)
modalView := modalStyle.
Width(outerWidth).
Render(finalContent)
// Calculate position for centering
bgHeight := lipgloss.Height(background)
bgWidth := lipgloss.Width(background)
modalHeight := lipgloss.Height(modalView)
modalWidth := lipgloss.Width(modalView)
row := (bgHeight - modalHeight) / 2
col := (bgWidth - modalWidth) / 2
return layout.PlaceOverlay(
col-1, // TODO: whyyyyy
row,
modalView,
background,
layout.WithOverlayBorder(),
layout.WithOverlayBorderColor(t.BorderActive()),
)
}