| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266 |
- 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...,
- )
- }
|