bloom
Layout, motion, and effects for Bubble Tea.
Describe your Bubble Tea view as a tree of rows, columns, and layers, built from your model every frame. bloom sizes it to the terminal, draws dialogs and toasts on top, animates what changes, and tells you what the mouse is over.
go get github.com/lrstanley/bloom@latestPre-1.0: expect breaking changes until v1.0.0.
What bloom takes off your hands
Bubble Tea gives you a model, messages, and a view that returns a string. Most of what makes a terminal app feel finished happens between those, and it's code you'd otherwise write for every program.
- Splitting the screen into panes
- Measure rendered strings, subtract borders and padding, and redo the math on every resize.
bloom.Row(bloom.Fixed(24, nav), bloom.Fill(main))- Drawing a dialog over the screen
- Strings can be joined side by side or stacked, not layered, so you splice lines by hand.
bloom.Overlay(screen, bloom.Layer(dialog).Modal())- Dimming what's behind it
- Re-render everything underneath in muted colors, and undo it when the dialog closes.
.Backdrop(fx.Dim{Amount: 0.6})- Showing a toast that hides itself
- A timer message, a flag to track it, and a tick loop to slide it in and out.
m.toast.ShowFor(2 * time.Second)- Showing data that's still loading
- Placeholder strings sized to match the content by hand, and a tick loop to shimmer them.
bloom.Skeleton(8, 0, 5, 11).Shimmer(t)- Knowing what was clicked
- Turn an X and Y back into whichever list item or button you drew there.
id, ok := msg.Target[noteID]()- Keeping focus inside a dialog
- Track tab order yourself, and remember where focus was before the dialog opened.
m.focus.Push(confirmDialog)- Animating anything
- Tick messages, easing math, and a little state machine for every moving part.
anim.NewPresence(anim.Spring(9, 0.35))- Fitting a narrow terminal
- Decide which panels to drop as the window shrinks, and bring them back as it grows.
bloom.Fixed(30, details).Collapsible(1)
One view, the whole screen
This is the View behind the terminal above. The layout, the dialog with its dimmed backdrop and iris entrance, and the toast in the corner are all described here, from model state. There's no separate place where layers are registered or animations are kept running.
- Slots decide how panes share space: fixed, filling, by weight, or by percentage, with minimums and panels that collapse.
- Layers sit over the screen, anchored to a corner or to another node, with backdrops, shadows, and modal input.
- Presences are animated values in your model. Show one, and the layer plays its effects in; hide it, and they play back out.
Components you already have, from Bubbles or your own, go anywhere in the tree as they are.
func (m *model) View() tea.View {
return m.ui.View(m.width, m.height, bloom.Overlay(
bloom.Column(
bloom.Fill(bloom.Row(
bloom.Fixed(24, sidebar).Collapsible(1),
bloom.Fill(reader).Min(30),
)),
statusbar,
),
bloom.Layer(dialog).
Modal().
Backdrop(fx.Dim{Amount: 0.6}).
Presence(m.confirm, fx.Iris{}),
bloom.Layer(toast).
Anchor(bloom.End, bloom.End).
Presence(m.toast,
fx.Slide{From: bloom.Right}, fx.Typewriter{}),
))
}Effects are values
Every effect takes a progress from 0 to 1. The same effect is a transition on a node, a backdrop behind a dialog, or a reaction to something happening, and effects combine in a list.
Where to start
New to Bubble Tea
Build a small notes app from an empty file: panes, clicks, then a dialog and a toast.
Bringing bloom to an existing app
Your model and components stay as they are. Anything with a View() string drops into a layout.
Looking something up
Every effect, node, and option on one page each, and the full API on pkg.go.dev.