diff options
| author | adamdottv <[email protected]> | 2025-06-12 09:35:19 -0500 |
|---|---|---|
| committer | adamdottv <[email protected]> | 2025-06-12 16:00:25 -0500 |
| commit | ca0ea3f94dc1929071efc93bfcf708c8cf054be8 (patch) | |
| tree | 294ec6778af8a712ae50ab5442c13ae95b51d337 /packages/tui/internal/components/modal | |
| parent | 98bd5109c20f7e83c2fd2c5ef0dc28796fc47315 (diff) | |
| download | opencode-ca0ea3f94dc1929071efc93bfcf708c8cf054be8.tar.gz opencode-ca0ea3f94dc1929071efc93bfcf708c8cf054be8.zip | |
wip: refactoring tui
Diffstat (limited to 'packages/tui/internal/components/modal')
| -rw-r--r-- | packages/tui/internal/components/modal/modal.go | 198 |
1 files changed, 198 insertions, 0 deletions
diff --git a/packages/tui/internal/components/modal/modal.go b/packages/tui/internal/components/modal/modal.go new file mode 100644 index 000000000..9477aee29 --- /dev/null +++ b/packages/tui/internal/components/modal/modal.go @@ -0,0 +1,198 @@ +package modal + +import ( + "github.com/charmbracelet/bubbles/v2/key" + tea "github.com/charmbracelet/bubbletea/v2" + "github.com/charmbracelet/lipgloss/v2" + "github.com/sst/opencode/internal/layout" + "github.com/sst/opencode/internal/styles" + "github.com/sst/opencode/internal/theme" +) + +// Modal is a reusable modal component that handles frame rendering and overlay placement +type Modal struct { + content tea.Model + width int + height int + title string + showBorder bool + borderStyle lipgloss.Border + maxWidth int + maxHeight int + centerContent 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 + } +} + +// WithBorder enables/disables the border +func WithBorder(show bool) ModalOption { + return func(m *Modal) { + m.showBorder = show + } +} + +// WithBorderStyle sets the border style +func WithBorderStyle(style lipgloss.Border) ModalOption { + return func(m *Modal) { + m.borderStyle = style + } +} + +// WithMaxWidth sets the maximum width +func WithMaxWidth(width int) ModalOption { + return func(m *Modal) { + m.maxWidth = width + } +} + +// WithMaxHeight sets the maximum height +func WithMaxHeight(height int) ModalOption { + return func(m *Modal) { + m.maxHeight = height + } +} + +// WithCenterContent centers the content within the modal +func WithCenterContent(center bool) ModalOption { + return func(m *Modal) { + m.centerContent = center + } +} + +// New creates a new Modal with the given content and options +func New(content tea.Model, opts ...ModalOption) *Modal { + m := &Modal{ + content: content, + showBorder: true, + borderStyle: lipgloss.ThickBorder(), + maxWidth: 0, + maxHeight: 0, + centerContent: false, + } + + for _, opt := range opts { + opt(m) + } + + return m +} + +func (m *Modal) Init() tea.Cmd { + return m.content.Init() +} + +func (m *Modal) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.width = msg.Width + m.height = msg.Height + } + + // Pass all messages to the content + var cmd tea.Cmd + m.content, cmd = m.content.Update(msg) + return m, cmd +} + +func (m *Modal) View() string { + t := theme.CurrentTheme() + + // Get the content view + contentView := "" + if v, ok := m.content.(layout.ModelWithView); ok { + contentView = v.View() + } + + // Calculate dimensions + outerWidth := layout.Current.Container.Width - 8 + if m.maxWidth > 0 && outerWidth > m.maxWidth { + outerWidth = m.maxWidth + } + + innerWidth := outerWidth - 4 + + // Base style for the modal + baseStyle := styles.BaseStyle(). + Background(t.BackgroundElement()). + Foreground(t.TextMuted()) + + // Add title if provided + var finalContent string + if m.title != "" { + titleStyle := baseStyle. + Foreground(t.Primary()). + Bold(true). + Width(innerWidth). + Padding(0, 1) + + titleView := titleStyle.Render(m.title) + finalContent = lipgloss.JoinVertical( + lipgloss.Left, + titleView, + contentView, + ) + } else { + finalContent = contentView + } + + // Apply modal styling + modalStyle := baseStyle. + PaddingTop(1). + PaddingBottom(1). + PaddingLeft(2). + PaddingRight(2) + + if m.showBorder { + modalStyle = modalStyle. + BorderStyle(m.borderStyle). + BorderLeft(true). + BorderRight(true). + BorderLeftForeground(t.BackgroundSubtle()). + BorderLeftBackground(t.Background()). + BorderRightForeground(t.BackgroundSubtle()). + BorderRightBackground(t.Background()) + } + + return modalStyle. + Width(outerWidth). + Render(finalContent) +} + +// Render renders the modal centered on the screen +func (m *Modal) Render(background string) string { + modalView := m.View() + + // 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 + + // Use PlaceOverlay to render the modal on top of the background + return layout.PlaceOverlay( + col, + row, + modalView, + background, + true, // shadow + ) +} + +// BindingKeys returns the key bindings from the content if it implements layout.Bindings +func (m *Modal) BindingKeys() []key.Binding { + if b, ok := m.content.(layout.Bindings); ok { + return b.BindingKeys() + } + return []key.Binding{} +}
\ No newline at end of file |
