diff options
| author | adamdottv <[email protected]> | 2025-06-19 10:12:19 -0500 |
|---|---|---|
| committer | adamdottv <[email protected]> | 2025-06-19 10:12:29 -0500 |
| commit | e1f12f93ebaf56eeabe19404c6411b2a92c4c8c4 (patch) | |
| tree | 345418fb5534c3f450555d5eeaddcfca9640d71f /packages/tui/internal/components | |
| parent | 7ca8334a8b39f19fe04e563189bc07c63253c256 (diff) | |
| download | opencode-e1f12f93ebaf56eeabe19404c6411b2a92c4c8c4.tar.gz opencode-e1f12f93ebaf56eeabe19404c6411b2a92c4c8c4.zip | |
feat(tui): toast messages
Diffstat (limited to 'packages/tui/internal/components')
| -rw-r--r-- | packages/tui/internal/components/modal/modal.go | 3 | ||||
| -rw-r--r-- | packages/tui/internal/components/toast/toast.go | 245 |
2 files changed, 246 insertions, 2 deletions
diff --git a/packages/tui/internal/components/modal/modal.go b/packages/tui/internal/components/modal/modal.go index e57de0afe..b2b59a4b2 100644 --- a/packages/tui/internal/components/modal/modal.go +++ b/packages/tui/internal/components/modal/modal.go @@ -100,8 +100,7 @@ func (m *Modal) Render(contentView string, background string) string { if m.title != "" { titleStyle := baseStyle. Foreground(t.Primary()). - Bold(true). - Padding(0, 1) + Bold(true) // titleView := titleStyle.Render(m.title) escStyle := baseStyle.Foreground(t.TextMuted()).Bold(false) diff --git a/packages/tui/internal/components/toast/toast.go b/packages/tui/internal/components/toast/toast.go new file mode 100644 index 000000000..9c13f4d9c --- /dev/null +++ b/packages/tui/internal/components/toast/toast.go @@ -0,0 +1,245 @@ +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 +} + +// View renders all active toasts +func (tm *ToastManager) View() string { + if len(tm.toasts) == 0 { + return "" + } + + t := theme.CurrentTheme() + + var toastViews []string + for _, toast := range tm.toasts { + baseStyle := styles.BaseStyle(). + Background(t.BackgroundElement()). + Foreground(t.Text()). + Padding(1, 2). + BorderStyle(lipgloss.ThickBorder()). + BorderBackground(t.Background()). + BorderForeground(toast.Color). + BorderLeft(true). + BorderRight(true) + + 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 := lipgloss.NewStyle(). + Foreground(toast.Color). + Bold(true) + content.WriteString(titleStyle.Render(*toast.Title)) + content.WriteString("\n") + } + + // Wrap message text + messageStyle := lipgloss.NewStyle().Width(contentMaxWidth) + content.WriteString(messageStyle.Render(toast.Message)) + + // Render toast with max width + toastView := baseStyle.MaxWidth(maxWidth).Render(content.String()) + toastViews = append(toastViews, toastView) + } + + // Stack toasts vertically with small gap + return strings.Join(toastViews, "\n\n") +} + +// RenderOverlay renders the toasts as an overlay on the given background +func (tm *ToastManager) RenderOverlay(background string) string { + toastView := tm.View() + if toastView == "" { + return background + } + + // Calculate position (bottom right with padding) + bgWidth := lipgloss.Width(background) + bgHeight := lipgloss.Height(background) + toastWidth := lipgloss.Width(toastView) + toastHeight := lipgloss.Height(toastView) + + // Position with 2 character padding from edges + x := bgWidth - toastWidth - 2 + y := bgHeight - toastHeight - 2 + + // Ensure we don't go negative + if x < 0 { + x = 0 + } + if y < 0 { + y = 0 + } + return layout.PlaceOverlay(x, y, toastView, background) +} + +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..., + ) +} |
