mirror of
https://github.com/anotherhadi/ilovetui.git
synced 2026-08-21 12:05:49 +02:00
init layout, notifications, tabs & more
Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
# notification
|
||||
|
||||
Toast-style notifications, triggered from anywhere in a bubbletea program via an exported
|
||||
`tea.Msg` (`ShowMsg`/`Show`) rather than a direct reference to the `Model` that ends up rendering
|
||||
them - standard Elm architecture, no IPC between processes.
|
||||
|
||||
It composites over an already-rendered string, so it has no dependency on
|
||||
`github.com/anotherhadi/ilovetui/layout`: the same `Model` works whether the host uses `layout`
|
||||
for its main content or not.
|
||||
|
||||
## Quick start
|
||||
|
||||
```go
|
||||
import (
|
||||
"github.com/anotherhadi/ilovetui/notification"
|
||||
)
|
||||
|
||||
type model struct {
|
||||
notif notification.Model
|
||||
width, height int
|
||||
}
|
||||
|
||||
func newModel() model {
|
||||
return model{notif: notification.New()}
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd { return m.notif.Init() }
|
||||
|
||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyPressMsg:
|
||||
if msg.String() == "s" {
|
||||
return m, notification.Show("Saved", "Config written to disk", notification.Success)
|
||||
}
|
||||
}
|
||||
|
||||
var cmd tea.Cmd
|
||||
m.notif, cmd = m.notif.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func (m model) View() tea.View {
|
||||
background := renderYourUI(m.width, m.height)
|
||||
view := tea.NewView(m.notif.Render(background))
|
||||
view.AltScreen = true
|
||||
return view
|
||||
}
|
||||
```
|
||||
|
||||
Any component in the same bubbletea program can trigger a toast via `notification.Show`, without
|
||||
holding a reference to the `notification.Model` that will actually render it - that `Model` just
|
||||
needs to see every `tea.Msg` the program produces (i.e. get its `Update` called from the top-level
|
||||
`Update`), same as any other child model.
|
||||
|
||||
## Showing and dismissing
|
||||
|
||||
```go
|
||||
return m, notification.Show("Saved", "Config written to disk", notification.Success)
|
||||
|
||||
return m, notification.Show("Sticky", "Stays until dismissed",
|
||||
notification.Info, notification.WithID("sticky-demo"), notification.WithDuration(0))
|
||||
return m, notification.Dismiss("sticky-demo")
|
||||
```
|
||||
|
||||
Four kinds: `Info`, `Success`, `Warning`, `Error`, each with its own color preset (see Styling
|
||||
below). By default a toast auto-dismisses after `notification.DefaultDuration` (3s);
|
||||
`WithDuration(0)` makes it sticky - it stays until `Dismiss(id)` removes it, so a sticky toast
|
||||
needs `WithID` to be dismissable later (an auto-generated id is never returned to the caller).
|
||||
Showing again with the same id replaces the toast in place, resetting its position and timer,
|
||||
instead of stacking a duplicate.
|
||||
|
||||
## Position and stacking
|
||||
|
||||
```go
|
||||
n := notification.New(notification.WithPosition(notification.TopRight))
|
||||
```
|
||||
|
||||
Six anchors: `Top`, `TopLeft`, `TopRight`, `Bottom`, `BottomLeft`, `BottomRight` - toasts always
|
||||
hug an edge or corner, never the middle of the screen. Multiple toasts stack along the anchored
|
||||
edge, newest closest to it; a stack that overflows the background's height clips the oldest
|
||||
toasts first, so the newest ones stay visible.
|
||||
|
||||
## Styling
|
||||
|
||||
```go
|
||||
n := notification.New(notification.WithMaxWidth(40), notification.WithStyles(myStyles))
|
||||
|
||||
return m, notification.Show("Title", "Message", notification.Success,
|
||||
notification.WithToastStyle(oneOffStyle))
|
||||
```
|
||||
|
||||
`WithMaxWidth` caps how wide a toast box can grow before its message wraps; a toast narrower than
|
||||
the cap shrinks to fit its content instead of padding out to it. A toast can also never overflow
|
||||
past the edge of whatever background it's rendered on, regardless of this cap. `WithStyles` sets
|
||||
the default per-`Kind` look for every toast shown by this `Model`; `WithToastStyle` (a `Show`
|
||||
option) overrides it for one toast alone. `DefaultStyles()` builds from `style.S`: `Info` uses
|
||||
`Primary` (no dedicated "info" color in the theme), `Success`/`Warning`/`Error` use their matching
|
||||
`style.S` alias.
|
||||
|
||||
## Examples
|
||||
|
||||
- `examples/notification` - all four kinds, a sticky toast with manual dismiss, cycling through
|
||||
all six positions.
|
||||
@@ -0,0 +1,115 @@
|
||||
// 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
|
||||
// has no dependency on github.com/anotherhadi/ilovetui/layout: the same
|
||||
// Model works whether the host uses layout for its main content or not (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
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package notification
|
||||
|
||||
// Position anchors the toast stack to one of six spots on the rendered
|
||||
// background. There's no center/middle variant: toasts always hug an edge or
|
||||
// a corner, never the middle of the screen.
|
||||
type Position int
|
||||
|
||||
const (
|
||||
Top Position = iota
|
||||
TopLeft
|
||||
TopRight
|
||||
Bottom
|
||||
BottomLeft
|
||||
BottomRight
|
||||
)
|
||||
|
||||
// margin is the fixed gap, in cells, kept between the toast stack and the
|
||||
// edge(s) of the background it's anchored to.
|
||||
const margin = 1
|
||||
|
||||
// placement resolves the top-left (x, y) coordinate to draw a stack of size
|
||||
// (sw, sh) at, given a background of size (w, h) and the anchor position.
|
||||
func placement(pos Position, w, h, sw, sh int) (x, y int) {
|
||||
switch pos {
|
||||
case Top:
|
||||
x = (w - sw) / 2
|
||||
y = margin
|
||||
case TopLeft:
|
||||
x = margin
|
||||
y = margin
|
||||
case TopRight:
|
||||
x = w - sw - margin
|
||||
y = margin
|
||||
case Bottom:
|
||||
x = (w - sw) / 2
|
||||
y = h - sh - margin
|
||||
case BottomLeft:
|
||||
x = margin
|
||||
y = h - sh - margin
|
||||
case BottomRight:
|
||||
x = w - sw - margin
|
||||
y = h - sh - margin
|
||||
}
|
||||
if x < 0 {
|
||||
x = 0
|
||||
}
|
||||
if y < 0 {
|
||||
y = 0
|
||||
}
|
||||
return x, y
|
||||
}
|
||||
|
||||
// anchoredTop reports whether pos hugs the top edge, which decides both the
|
||||
// stacking order (see Model.orderedToasts) and which side of an overflowing
|
||||
// stack gets clipped (see clipToHeight).
|
||||
func (pos Position) anchoredTop() bool {
|
||||
return pos == Top || pos == TopLeft || pos == TopRight
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
package notification
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"charm.land/lipgloss/v2"
|
||||
|
||||
"github.com/anotherhadi/ilovetui/style"
|
||||
)
|
||||
|
||||
// Render composites the current toasts on top of background (already
|
||||
// rendered, e.g. layout.Model.View() or any other component's View()) and
|
||||
// returns the result. background is returned unchanged whenever there's
|
||||
// nothing to draw (no toasts, or a background with no measurable size).
|
||||
//
|
||||
// This is what makes notification work identically with or without layout:
|
||||
// the host just wraps whatever it would otherwise return from its own
|
||||
// View() with this call.
|
||||
func (m Model) Render(background string) string {
|
||||
if len(m.toasts) == 0 {
|
||||
return background
|
||||
}
|
||||
w, h := lipgloss.Width(background), lipgloss.Height(background)
|
||||
if w <= 0 || h <= 0 {
|
||||
return background
|
||||
}
|
||||
|
||||
stack := clipToHeight(m.renderStack(effectiveMaxWidth(m.maxWidth, w)), h-2*margin, m.position.anchoredTop())
|
||||
if stack == "" {
|
||||
return background
|
||||
}
|
||||
sw, sh := lipgloss.Width(stack), lipgloss.Height(stack)
|
||||
x, y := placement(m.position, w, h, sw, sh)
|
||||
|
||||
// Canvas.Compose(layer) alone ignores the layer's X/Y and draws it across
|
||||
// the canvas's whole bounds, not just its own footprint - that's what
|
||||
// made the toast layer blank out the entire background instead of
|
||||
// floating over it. Compositor is what actually resolves each layer's
|
||||
// absolute bounds (background at 0,0, the stack at x,y) before drawing
|
||||
// each one only within its own area.
|
||||
compositor := lipgloss.NewCompositor(
|
||||
lipgloss.NewLayer(background),
|
||||
lipgloss.NewLayer(stack).X(x).Y(y).Z(1),
|
||||
)
|
||||
return compositor.Render()
|
||||
}
|
||||
|
||||
// View is a convenience for a pane whose sole purpose is showing toasts (e.g.
|
||||
// a dedicated layout.Leaf): it draws the stack over a blank width x height
|
||||
// area instead of an existing background.
|
||||
func (m Model) View(width, height int) string {
|
||||
return m.Render(blank(width, height))
|
||||
}
|
||||
|
||||
// renderStack stacks every visible toast into one block, newest closest to
|
||||
// the anchored edge (see Position.anchoredTop), separated by a blank line,
|
||||
// and aligned so the edge the stack anchors to stays flush across toasts of
|
||||
// different widths.
|
||||
func (m Model) renderStack(maxWidth int) string {
|
||||
ordered := m.orderedToasts()
|
||||
parts := make([]string, 0, len(ordered)*2-1)
|
||||
for i, t := range ordered {
|
||||
if i > 0 {
|
||||
parts = append(parts, "")
|
||||
}
|
||||
parts = append(parts, m.renderToast(t, maxWidth))
|
||||
}
|
||||
return lipgloss.JoinVertical(stackAlign(m.position), parts...)
|
||||
}
|
||||
|
||||
// orderedToasts returns the toasts in the order they should stack, newest
|
||||
// nearest the anchored edge: reversed (newest first) for a top anchor,
|
||||
// insertion order (oldest first, newest last) for a bottom anchor.
|
||||
func (m Model) orderedToasts() []Toast {
|
||||
if !m.position.anchoredTop() {
|
||||
return m.toasts
|
||||
}
|
||||
ordered := make([]Toast, len(m.toasts))
|
||||
for i, t := range m.toasts {
|
||||
ordered[len(m.toasts)-1-i] = t
|
||||
}
|
||||
return ordered
|
||||
}
|
||||
|
||||
func stackAlign(pos Position) lipgloss.Position {
|
||||
switch pos {
|
||||
case TopLeft, BottomLeft:
|
||||
return lipgloss.Left
|
||||
case TopRight, BottomRight:
|
||||
return lipgloss.Right
|
||||
default:
|
||||
return lipgloss.Center
|
||||
}
|
||||
}
|
||||
|
||||
// renderToast draws a single toast as a box with its title embedded in the
|
||||
// top border (style.RenderWithTitle), shrunk to fit its content up to
|
||||
// maxWidth.
|
||||
func (m Model) renderToast(t Toast, maxWidth int) string {
|
||||
k := m.styles.forKind(t)
|
||||
|
||||
inner := contentWidth(t, maxWidth)
|
||||
message := k.Message.Width(inner).Render(t.Message)
|
||||
|
||||
boxWidth := inner + 4 // border (2) + Padding(0, 1) (2)
|
||||
boxHeight := lipgloss.Height(message) + 2
|
||||
|
||||
return style.RenderWithTitle(k.Border, k.Title.Render(t.Title), message, boxWidth, boxHeight)
|
||||
}
|
||||
|
||||
// contentWidth is the toast's inner (border/padding excluded) width: its
|
||||
// natural size (long enough for the wider of title/message on one line),
|
||||
// capped at maxWidth if positive.
|
||||
func contentWidth(t Toast, maxWidth int) int {
|
||||
natural := max(lipgloss.Width(t.Title), lipgloss.Width(t.Message), 1)
|
||||
if maxWidth <= 0 {
|
||||
return natural
|
||||
}
|
||||
capped := max(maxWidth-4, 1)
|
||||
return min(natural, capped)
|
||||
}
|
||||
|
||||
// effectiveMaxWidth resolves the cap actually used to render a toast:
|
||||
// configured (Model.maxWidth, 0 = unlimited) narrowed down to whatever
|
||||
// actually fits the background it's about to be drawn on, so a toast can
|
||||
// never overflow past the edge of the background - or the terminal, when
|
||||
// the background is a full-screen View() - regardless of how WithMaxWidth
|
||||
// was set. bgWidth is background's own width, already measured by Render.
|
||||
func effectiveMaxWidth(configured, bgWidth int) int {
|
||||
fits := max(bgWidth-2*margin, 1)
|
||||
if configured > 0 && configured < fits {
|
||||
return configured
|
||||
}
|
||||
return fits
|
||||
}
|
||||
|
||||
// clipToHeight trims stack to at most maxHeight lines when it overflows,
|
||||
// keeping the lines nearest the anchored edge (top rows for a top anchor,
|
||||
// bottom rows for a bottom anchor) so the newest toasts - always nearest
|
||||
// that edge, see orderedToasts - are the ones that stay visible.
|
||||
func clipToHeight(stack string, maxHeight int, anchoredTop bool) string {
|
||||
lines := strings.Split(stack, "\n")
|
||||
if len(lines) <= maxHeight {
|
||||
return stack
|
||||
}
|
||||
if maxHeight <= 0 {
|
||||
return ""
|
||||
}
|
||||
if anchoredTop {
|
||||
lines = lines[:maxHeight]
|
||||
} else {
|
||||
lines = lines[len(lines)-maxHeight:]
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func blank(width, height int) string {
|
||||
if width <= 0 || height <= 0 {
|
||||
return ""
|
||||
}
|
||||
line := strings.Repeat(" ", width)
|
||||
lines := make([]string, height)
|
||||
for i := range lines {
|
||||
lines[i] = line
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package notification
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
|
||||
"charm.land/lipgloss/v2"
|
||||
|
||||
"github.com/anotherhadi/ilovetui/style"
|
||||
)
|
||||
|
||||
// KindStyle is the set of lipgloss styles used to render one toast: Border
|
||||
// carries the box's border (shape + color, no size), Title and Message color
|
||||
// the two pieces of text drawn inside it. Building a value directly (rather
|
||||
// than through a constructor) is the intended way to hand WithToastStyle a
|
||||
// custom, per-toast look.
|
||||
type KindStyle struct {
|
||||
Border lipgloss.Style
|
||||
Title lipgloss.Style
|
||||
Message lipgloss.Style
|
||||
}
|
||||
|
||||
// Styles maps each Kind to the KindStyle used to render it. Build one with
|
||||
// DefaultStyles and tweak individual fields, or construct one from scratch
|
||||
// for a fully custom palette across all kinds.
|
||||
type Styles struct {
|
||||
Info KindStyle
|
||||
Success KindStyle
|
||||
Warning KindStyle
|
||||
Error KindStyle
|
||||
}
|
||||
|
||||
// DefaultStyles builds a Styles from style.S: Info uses the theme's primary
|
||||
// accent (style.S has no dedicated "info" color, Primary already fills that
|
||||
// neutral-accent role elsewhere in this repo), Success/Warning/Error use
|
||||
// their matching style.S alias.
|
||||
func DefaultStyles() Styles {
|
||||
return Styles{
|
||||
Info: kindStyle(style.S.Primary),
|
||||
Success: kindStyle(style.S.Success),
|
||||
Warning: kindStyle(style.S.Warning),
|
||||
Error: kindStyle(style.S.Error),
|
||||
}
|
||||
}
|
||||
|
||||
func kindStyle(c color.Color) KindStyle {
|
||||
return KindStyle{
|
||||
Border: lipgloss.NewStyle().
|
||||
Border(style.S.BorderType).
|
||||
BorderForeground(c).
|
||||
Padding(0, 1),
|
||||
Title: lipgloss.NewStyle().Bold(true).Foreground(c),
|
||||
Message: lipgloss.NewStyle().Foreground(style.S.Text),
|
||||
}
|
||||
}
|
||||
|
||||
// forKind resolves the KindStyle to render t with: its own Style override if
|
||||
// set, otherwise s's preset for t.Kind (falling back to Info for an
|
||||
// out-of-range Kind).
|
||||
func (s Styles) forKind(t Toast) KindStyle {
|
||||
if t.Style != nil {
|
||||
return *t.Style
|
||||
}
|
||||
switch t.Kind {
|
||||
case Success:
|
||||
return s.Success
|
||||
case Warning:
|
||||
return s.Warning
|
||||
case Error:
|
||||
return s.Error
|
||||
default:
|
||||
return s.Info
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package notification
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
)
|
||||
|
||||
// Kind picks which of Styles' presets a toast renders with, unless
|
||||
// overridden per-toast via WithToastStyle.
|
||||
type Kind int
|
||||
|
||||
const (
|
||||
Info Kind = iota
|
||||
Success
|
||||
Warning
|
||||
Error
|
||||
)
|
||||
|
||||
// DefaultDuration is how long a toast stays visible when WithDuration isn't
|
||||
// used. Show has no reference to a Model (see ShowMsg's doc comment), so this
|
||||
// lives as a package constant rather than a Model-level default.
|
||||
const DefaultDuration = 3 * time.Second
|
||||
|
||||
// Toast is one notification. Build it via Show's opts rather than a literal:
|
||||
// ID and Duration both get defaults (see WithID, DefaultDuration) that a bare
|
||||
// literal would silently skip.
|
||||
type Toast struct {
|
||||
ID string
|
||||
Title string
|
||||
Message string
|
||||
Kind Kind
|
||||
// Duration is how long the toast stays up before auto-dismissing. 0
|
||||
// means sticky: it stays until DismissMsg/Dismiss(ID) removes it.
|
||||
Duration time.Duration
|
||||
// Style, if non-nil, overrides the Model's Kind-based preset for this
|
||||
// toast alone.
|
||||
Style *KindStyle
|
||||
}
|
||||
|
||||
// ToastOption configures a Toast built by Show.
|
||||
type ToastOption func(*Toast)
|
||||
|
||||
// WithID gives the toast a stable id, so a later Show reusing the same id
|
||||
// replaces it in place (resetting its position and timer) instead of
|
||||
// stacking a duplicate, and so it can be targeted by Dismiss.
|
||||
func WithID(id string) ToastOption {
|
||||
return func(t *Toast) { t.ID = id }
|
||||
}
|
||||
|
||||
// WithDuration overrides DefaultDuration. 0 makes the toast sticky: it never
|
||||
// auto-dismisses, only Dismiss(ID) removes it.
|
||||
func WithDuration(d time.Duration) ToastOption {
|
||||
return func(t *Toast) { t.Duration = d }
|
||||
}
|
||||
|
||||
// WithToastStyle overrides the Model's Kind-based preset for this toast
|
||||
// alone, for a one-off custom look instead of the type-based theme.
|
||||
func WithToastStyle(s KindStyle) ToastOption {
|
||||
return func(t *Toast) { t.Style = &s }
|
||||
}
|
||||
|
||||
func newToast(title, message string, kind Kind, opts ...ToastOption) Toast {
|
||||
t := Toast{
|
||||
Title: title,
|
||||
Message: message,
|
||||
Kind: kind,
|
||||
Duration: DefaultDuration,
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(&t)
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// ShowMsg tells a notification.Model to display Toast. Any component in the
|
||||
// same bubbletea program can trigger one via Show, without holding a
|
||||
// reference to the notification.Model that will actually render it - that
|
||||
// Model just needs to see every tea.Msg the program produces, same as any
|
||||
// other child model.
|
||||
type ShowMsg struct{ Toast Toast }
|
||||
|
||||
// Show returns a tea.Cmd that shows a new toast of the given kind. Call it
|
||||
// from any component's Update:
|
||||
//
|
||||
// return m, notification.Show("Saved", "Config written to disk", notification.Success)
|
||||
func Show(title, message string, kind Kind, opts ...ToastOption) tea.Cmd {
|
||||
t := newToast(title, message, kind, opts...)
|
||||
return func() tea.Msg { return ShowMsg{Toast: t} }
|
||||
}
|
||||
|
||||
// DismissMsg removes the toast identified by ID, whether it's sticky or
|
||||
// mid-countdown. A no-op if ID isn't currently shown (already expired, or
|
||||
// never had an explicit id in the first place - see WithID).
|
||||
type DismissMsg struct{ ID string }
|
||||
|
||||
// Dismiss returns a tea.Cmd that removes the toast identified by id. Only
|
||||
// useful for toasts shown with WithID, since an auto-generated id is never
|
||||
// exposed back to the caller.
|
||||
func Dismiss(id string) tea.Cmd {
|
||||
return func() tea.Msg { return DismissMsg{ID: id} }
|
||||
}
|
||||
|
||||
// expireMsg fires once a toast's Duration has elapsed, scheduled by
|
||||
// Model.show via tea.Tick. Unexported: nothing outside the package should
|
||||
// construct or match on it directly, that's what DismissMsg is for.
|
||||
type expireMsg struct{ id string }
|
||||
Reference in New Issue
Block a user