mirror of
https://github.com/sst/opencode.git
synced 2025-08-23 14:34:08 +00:00
266 lines
5.9 KiB
Go
266 lines
5.9 KiB
Go
package toast
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
tea "github.com/charmbracelet/bubbletea/v2"
|
|
"github.com/charmbracelet/lipgloss/v2"
|
|
"github.com/charmbracelet/lipgloss/v2/compat"
|
|
"github.com/sst/opencode/internal/layout"
|
|
"github.com/sst/opencode/internal/styles"
|
|
"github.com/sst/opencode/internal/theme"
|
|
)
|
|
|
|
// ShowToastMsg is a message to display a toast notification
|
|
type ShowToastMsg struct {
|
|
Message string
|
|
Title *string
|
|
Color compat.AdaptiveColor
|
|
Duration time.Duration
|
|
}
|
|
|
|
// DismissToastMsg is a message to dismiss a specific toast
|
|
type DismissToastMsg struct {
|
|
ID string
|
|
}
|
|
|
|
// Toast represents a single toast notification
|
|
type Toast struct {
|
|
ID string
|
|
Message string
|
|
Title *string
|
|
Color compat.AdaptiveColor
|
|
CreatedAt time.Time
|
|
Duration time.Duration
|
|
}
|
|
|
|
// ToastManager manages multiple toast notifications
|
|
type ToastManager struct {
|
|
toasts []Toast
|
|
}
|
|
|
|
// NewToastManager creates a new toast manager
|
|
func NewToastManager() *ToastManager {
|
|
return &ToastManager{
|
|
toasts: []Toast{},
|
|
}
|
|
}
|
|
|
|
// Init initializes the toast manager
|
|
func (tm *ToastManager) Init() tea.Cmd {
|
|
return nil
|
|
}
|
|
|
|
// Update handles messages for the toast manager
|
|
func (tm *ToastManager) Update(msg tea.Msg) (*ToastManager, tea.Cmd) {
|
|
switch msg := msg.(type) {
|
|
case ShowToastMsg:
|
|
toast := Toast{
|
|
ID: fmt.Sprintf("toast-%d", time.Now().UnixNano()),
|
|
Title: msg.Title,
|
|
Message: msg.Message,
|
|
Color: msg.Color,
|
|
CreatedAt: time.Now(),
|
|
Duration: msg.Duration,
|
|
}
|
|
|
|
tm.toasts = append(tm.toasts, toast)
|
|
|
|
// Return command to dismiss after duration
|
|
return tm, tea.Tick(toast.Duration, func(t time.Time) tea.Msg {
|
|
return DismissToastMsg{ID: toast.ID}
|
|
})
|
|
|
|
case DismissToastMsg:
|
|
var newToasts []Toast
|
|
for _, t := range tm.toasts {
|
|
if t.ID != msg.ID {
|
|
newToasts = append(newToasts, t)
|
|
}
|
|
}
|
|
tm.toasts = newToasts
|
|
}
|
|
|
|
return tm, nil
|
|
}
|
|
|
|
// renderSingleToast renders a single toast notification
|
|
func (tm *ToastManager) renderSingleToast(toast Toast) string {
|
|
t := theme.CurrentTheme()
|
|
|
|
baseStyle := styles.NewStyle().
|
|
Foreground(t.Text()).
|
|
Background(t.BackgroundElement()).
|
|
Padding(1, 2)
|
|
|
|
maxWidth := max(40, layout.Current.Viewport.Width/3)
|
|
contentMaxWidth := max(maxWidth-6, 20)
|
|
|
|
// Build content with wrapping
|
|
var content strings.Builder
|
|
if toast.Title != nil {
|
|
titleStyle := styles.NewStyle().Foreground(toast.Color).
|
|
Bold(true)
|
|
content.WriteString(titleStyle.Render(*toast.Title))
|
|
content.WriteString("\n")
|
|
}
|
|
|
|
// Wrap message text
|
|
messageStyle := styles.NewStyle()
|
|
contentWidth := lipgloss.Width(toast.Message)
|
|
if contentWidth > contentMaxWidth {
|
|
messageStyle = messageStyle.Width(contentMaxWidth)
|
|
}
|
|
content.WriteString(messageStyle.Render(toast.Message))
|
|
|
|
// Render toast with max width
|
|
return baseStyle.MaxWidth(maxWidth).Render(content.String())
|
|
}
|
|
|
|
// View renders all active toasts
|
|
func (tm *ToastManager) View() string {
|
|
if len(tm.toasts) == 0 {
|
|
return ""
|
|
}
|
|
|
|
var toastViews []string
|
|
for _, toast := range tm.toasts {
|
|
toastView := tm.renderSingleToast(toast)
|
|
toastViews = append(toastViews, toastView+"\n")
|
|
}
|
|
|
|
return strings.Join(toastViews, "\n")
|
|
}
|
|
|
|
// RenderOverlay renders the toasts as an overlay on the given background
|
|
func (tm *ToastManager) RenderOverlay(background string) string {
|
|
if len(tm.toasts) == 0 {
|
|
return background
|
|
}
|
|
|
|
bgWidth := lipgloss.Width(background)
|
|
bgHeight := lipgloss.Height(background)
|
|
result := background
|
|
|
|
// Start from top with 2 character padding
|
|
currentY := 2
|
|
|
|
// Render each toast individually
|
|
for _, toast := range tm.toasts {
|
|
// Render individual toast
|
|
toastView := tm.renderSingleToast(toast)
|
|
toastWidth := lipgloss.Width(toastView)
|
|
toastHeight := lipgloss.Height(toastView)
|
|
|
|
// Position at top-right with 2 character padding from right edge
|
|
x := max(bgWidth-toastWidth-4, 0)
|
|
|
|
// Check if toast fits vertically
|
|
if currentY+toastHeight > bgHeight-2 {
|
|
// No more room for toasts
|
|
break
|
|
}
|
|
|
|
// Place this toast
|
|
result = layout.PlaceOverlay(
|
|
x,
|
|
currentY,
|
|
toastView,
|
|
result,
|
|
layout.WithOverlayBorder(),
|
|
layout.WithOverlayBorderColor(toast.Color),
|
|
)
|
|
|
|
// Move down for next toast (add 1 for spacing between toasts)
|
|
currentY += toastHeight + 1
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
type ToastOptions struct {
|
|
Title string
|
|
Duration time.Duration
|
|
}
|
|
|
|
type toastOptions struct {
|
|
title *string
|
|
duration *time.Duration
|
|
color *compat.AdaptiveColor
|
|
}
|
|
|
|
type ToastOption func(*toastOptions)
|
|
|
|
func WithTitle(title string) ToastOption {
|
|
return func(t *toastOptions) {
|
|
t.title = &title
|
|
}
|
|
}
|
|
func WithDuration(duration time.Duration) ToastOption {
|
|
return func(t *toastOptions) {
|
|
t.duration = &duration
|
|
}
|
|
}
|
|
|
|
func WithColor(color compat.AdaptiveColor) ToastOption {
|
|
return func(t *toastOptions) {
|
|
t.color = &color
|
|
}
|
|
}
|
|
|
|
func NewToast(message string, options ...ToastOption) tea.Cmd {
|
|
t := theme.CurrentTheme()
|
|
duration := 5 * time.Second
|
|
color := t.Primary()
|
|
|
|
opts := toastOptions{
|
|
duration: &duration,
|
|
color: &color,
|
|
}
|
|
for _, option := range options {
|
|
option(&opts)
|
|
}
|
|
|
|
return func() tea.Msg {
|
|
return ShowToastMsg{
|
|
Message: message,
|
|
Title: opts.title,
|
|
Duration: *opts.duration,
|
|
Color: *opts.color,
|
|
}
|
|
}
|
|
}
|
|
|
|
func NewInfoToast(message string, options ...ToastOption) tea.Cmd {
|
|
options = append(options, WithColor(theme.CurrentTheme().Info()))
|
|
return NewToast(
|
|
message,
|
|
options...,
|
|
)
|
|
}
|
|
|
|
func NewSuccessToast(message string, options ...ToastOption) tea.Cmd {
|
|
options = append(options, WithColor(theme.CurrentTheme().Success()))
|
|
return NewToast(
|
|
message,
|
|
options...,
|
|
)
|
|
}
|
|
|
|
func NewWarningToast(message string, options ...ToastOption) tea.Cmd {
|
|
options = append(options, WithColor(theme.CurrentTheme().Warning()))
|
|
return NewToast(
|
|
message,
|
|
options...,
|
|
)
|
|
}
|
|
|
|
func NewErrorToast(message string, options ...ToastOption) tea.Cmd {
|
|
options = append(options, WithColor(theme.CurrentTheme().Error()))
|
|
return NewToast(
|
|
message,
|
|
options...,
|
|
)
|
|
}
|