Files
Hadi aa6496901f up
Signed-off-by: Hadi <hadi@example.fr>
2026-08-18 16:15:03 +02:00

116 lines
2.8 KiB
Go

// Package notification renders toast-style notifications, triggered from
// anywhere in a bubbletea program via an exported tea.Msg (see ShowMsg/Show)
// rather than a direct reference to the Model that ends up rendering them.
//
// It composites over an already-rendered string (see Model.Render), so it
// makes no assumption at all about how the host builds that string: the same
// Model works whatever the host uses to lay out its main content (see
// Model.Render and Model.View).
package notification
import (
"fmt"
"time"
tea "charm.land/bubbletea/v2"
)
// Model holds the currently visible toasts and the rendering config
// (position, max width, per-Kind styles) they share. Build one with New.
type Model struct {
toasts []Toast
nextID int
position Position
maxWidth int
styles Styles
}
// Option configures a Model at construction. See WithPosition, WithMaxWidth,
// WithStyles.
type Option func(*Model)
// WithPosition sets which edge/corner the toast stack anchors to. TopRight
// by default.
func WithPosition(p Position) Option {
return func(m *Model) { m.position = p }
}
// WithMaxWidth caps how wide a toast box can grow before its message
// wraps. A toast narrower than this shrinks to fit its content instead of
// padding out to the cap. 0 (also the zero-value Model's default without
// New) means unlimited.
func WithMaxWidth(w int) Option {
return func(m *Model) { m.maxWidth = w }
}
// WithStyles overrides the default per-Kind styles (see DefaultStyles).
func WithStyles(s Styles) Option {
return func(m *Model) { m.styles = s }
}
// New builds a Model. Defaults: TopRight, a 40-cell max width, DefaultStyles.
func New(opts ...Option) Model {
m := Model{
position: TopRight,
maxWidth: 40,
styles: DefaultStyles(),
}
for _, opt := range opts {
opt(&m)
}
return m
}
func (m Model) Init() tea.Cmd { return nil }
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
switch msg := msg.(type) {
case ShowMsg:
return m.show(msg.Toast)
case DismissMsg:
return m.remove(msg.ID), nil
case expireMsg:
return m.remove(msg.id), nil
}
return m, nil
}
// show adds or replaces (see WithID) a toast, and schedules its expiry via
// tea.Tick if it isn't sticky (Duration <= 0).
func (m Model) show(t Toast) (Model, tea.Cmd) {
if t.ID == "" {
t.ID = fmt.Sprintf("toast-%d", m.nextID)
m.nextID++
}
replaced := false
for i, existing := range m.toasts {
if existing.ID == t.ID {
m.toasts[i] = t
replaced = true
break
}
}
if !replaced {
m.toasts = append(m.toasts, t)
}
if t.Duration <= 0 {
return m, nil
}
id := t.ID
return m, tea.Tick(t.Duration, func(time.Time) tea.Msg {
return expireMsg{id: id}
})
}
func (m Model) remove(id string) Model {
for i, t := range m.toasts {
if t.ID == id {
m.toasts = append(m.toasts[:i], m.toasts[i+1:]...)
break
}
}
return m
}