Bubbles v2 is here! ๐ซง
We're thrilled to share Bubbles v2 with you! This release accompanies Bubble Tea v2 and Lip Gloss v2 and brings a ton of consistency, new features, and quality-of-life improvements across every component. Catch 'em all:
go get charm.land/bubbletea/v2
go get charm.land/bubbles/v2
go get charm.land/lipgloss/v2You can also check the Upgrade Guide for more info.
There are a lot of changes in here, but we've found upgrading pretty easy, especially with a linter. Read on for the full breakdown!
Note
When in doubt, check the examples for reference โ they've all been updated for v2.
๐ New Home
Bubbles v2 now lives at charm.land:
import "charm.land/bubbles/v2"All sub-packages follow the same pattern: charm.land/bubbles/v2/viewport, charm.land/bubbles/v2/list, etc.
๐จ Light and Dark Styles
Some Bubbles, like help, offer default styles for both light and dark backgrounds. Since Lip Gloss v2 removes AdaptiveColor, choosing light or dark is now a manual process. You've got a couple of options.
๐ฉ The Best Way
Have Bubble Tea query the background color for you. This properly queries the correct inputs and outputs, and happens in lockstep with your application:
func (m model) Init() tea.Cmd {
return tea.RequestBackgroundColor
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.BackgroundColorMsg:
m.help.Styles = help.DefaultStyles(msg.IsDark())
return m, nil
}
// ...
}If you're using Wish you must do it this way to get the background color of the client.
๐ค The Quick Way
Use the compat package in Lip Gloss. It's less recommended because it contains blocking I/O that operates independently of Bubble Tea, and when used with Wish it won't detect the client's background:
import "charm.land/lipgloss/v2/compat"
var hasDarkBG = compat.HasDarkBackground()
h := help.New()
h.Styles = help.DefaultStyles(hasDarkBG)๐ Or Just Pick One
h.Styles = help.DefaultLightStyles() // light mode!
h.Styles = help.DefaultDarkStyles() // jk dark modeThis pattern applies to help, list, textarea, and textinput.
๐ The Init You Know and Love is Back
After experimenting with a few different forms of Init during the alphas, we decided the v1 signature was the right call after all. Init was a bit too redundant for our tastes given that initialization already happens in New:
func (m Model) Init() tea.Cmdโจ The Big Highlights
Getters and Setters Everywhere
All components now use getter/setter methods instead of exported Width and Height fields. This lets us do internal bookkeeping when things change, and it makes the API consistent across every Bubble:
// Before
vp.Width = 40
fmt.Println(vp.Width)
// After
vp.SetWidth(40)
fmt.Println(vp.Width())Affected: filepicker, help, progress, table, textinput, viewport.
Functional Options
Constructors now use the functional options pattern instead of positional args or separate constructor functions:
vp := viewport.New(viewport.WithWidth(80), viewport.WithHeight(24))
sw := stopwatch.New(stopwatch.WithInterval(500 * time.Millisecond))
t := timer.New(30*time.Second, timer.WithInterval(100*time.Millisecond))DefaultKeyMap is a Function Now
All DefaultKeyMap package-level variables are now functions, so you get fresh values every time:
km := textinput.DefaultKeyMap() // was textinput.DefaultKeyMap
km := textarea.DefaultKeyMap() // was textarea.DefaultKeyMap
km := paginator.DefaultKeyMap() // was paginator.DefaultKeyMapReal Cursor Support ๐ฑ๏ธ
Both textarea and textinput now support real terminal cursors! The feature is opt-in, so by default your programs will continue to use the easy-breezy virtual cursor. Set VirtualCursor to false and use Model.Cursor() for the real deal. Check out the textarea and textinput examples to see it in action.
Cleaned House ๐งน
All previously deprecated symbols have been removed:
NewModelvariants โ just useNewspinner.Tick()โ useModel.Tick()insteadpaginator.UsePgUpPgDownKeysand friends โ customizeKeyMapdirectlyfilepicker.DefaultStylesWithRenderer()โ Lip Gloss is pure now, useDefaultStyles()viewport.HighPerformanceRenderingโ no longer neededruneutilandmemoizationpackages moved tointernal/(they were never meant for public use anyway)
What's Changed: the Laundry List
๐ฎ Cursor
Model.Blinkrenamed toModel.IsBlinkedfor clarityModel.BlinkCmd()renamed toModel.Blink()- Each cursor now gets a unique ID
๐ Filepicker
DefaultStylesWithRenderer()removed โ Lip Gloss is pure now, so just useDefaultStyles()Model.Heightbroken intoSetHeight(int)/Height() int
โ Help
Model.Widthbroken intoSetWidth(int)/Width() int- New
DefaultStyles(isDark bool),DefaultDarkStyles(), andDefaultLightStyles() - Defaults to dark background styles out of the box
๐ฅ List
DefaultStyles()andNewDefaultItemStyles()now take anisDark boolparameterStyles.FilterPromptandStyles.FilterCursorhave been consolidated intoStyles.Filter(atextinput.Styles)GlobalIndexhelper added
๐ Paginator
DefaultKeyMapvariable โDefaultKeyMap()function- Deprecated fields (
UsePgUpPgDownKeys,UseLeftRightKeys, etc.) removed โ customizeKeyMapdirectly
๐ Progress
This one got the biggest makeover!
- Complete color API overhaul:
WithGradient/WithScaledGradientโWithColors(...color.Color)โ pass 2+ colors for blendingWithSolidFill(string)โWithColors(color)โ pass a single color for a solid fillWithDefaultGradient()โWithDefaultBlend()- New
WithScaled(bool)to scale the blend to fit only the filled portion - New
WithColorFunc(func(total, current float64) color.Color)for fully dynamic coloring WithColorProfileremoved โ Bubble Tea handles this automatically now
Model.FullColorandModel.EmptyColorchanged fromstringtoimage/color.ColorModel.Widthbroken intoSetWidth(int)/Width() intModel.Updatenow returnsModelinstead oftea.Model- Improved blend algorithm with support for multiple color stops โ special thanks to the legendary @lrstanley!
๐ Spinner
Tick()package-level function removed โ useModel.Tick()instead
โฑ๏ธ Stopwatch
NewWithInterval(d)removed โ useNew(WithInterval(d))instead- Debounced tick messages
๐ข Table
Model.Width/Model.Heightreplaced with getter/setter methods- Uses
ansi.Truncateinstead ofrunewidth.Truncate - Fixed a critical out-of-bounds cursor bug โ thanks @s0ders!
โ๏ธ Textarea
The big change here is real cursor support โ but that's opt-in, so by default your programs will keep using the virtual cursor.
DefaultKeyMapvariable โDefaultKeyMap()function- New
PageUp/PageDownkey bindings Model.FocusedStyle/Model.BlurredStyleโModel.Styles.Focused/Model.Styles.BlurredStyletype renamed toStyleState; newStylesstruct groupsFocused,Blurred, andCursorModel.SetCursorrenamed toModel.SetCursorColumnModel.Cursoris nowfunc() *tea.Cursorfor real cursor supportModel.VirtualCursorbool added โ set tofalsewhen using a real cursor- New
DefaultStyles(isDark bool),DefaultDarkStyles(),DefaultLightStyles() - New methods:
Column(),ScrollYOffset(),ScrollPosition(),MoveToBeginning(),MoveToEnd() - Focus status now passed to
SetPromptFunc
๐ Textinput
Most of the changes here bring textinput to parity with textarea, including real cursor support. Styling has been consolidated into a Styles struct with Focused and Blurred states:
DefaultKeyMapvariable โDefaultKeyMap()functionModel.Widthbroken intoSetWidth(int)/Width() intModel.PromptStyleโStyleState.PromptModel.TextStyleโStyleState.TextModel.PlaceholderStyleโStyleState.PlaceholderModel.CompletionStyleโStyleState.SuggestionModel.Cursoris nowfunc() *tea.Cursorfor real cursor supportModel.VirtualCursor()/SetVirtualCursor(bool)addedModel.Styles()/SetStyles(Styles)added- New
DefaultStyles(isDark bool),DefaultDarkStyles(),DefaultLightStyles() - Exposed matched suggestions and suggestion index
โฒ๏ธ Timer
NewWithInterval(timeout, interval)removed โ useNew(timeout, WithInterval(interval))- Debounced tick messages
๐ฆ Viewport
viewport got a ton of love in v2. Let's dive in!
Breaking changes:
New(width, height int)โNew(...Option)withWithWidth/WithHeightModel.Width,Model.Height,Model.YOffsetreplaced with getter/setter methodsHighPerformanceRenderingremoved
Shiny new features:
You can now scroll horizontally with the left and right arrow keys, and set up a custom gutter column for things like line numbers:
vp := viewport.New()
vp.SetContent("hello world")
// Show line numbers:
vp.LeftGutterFunc = func(info viewport.GutterContext) string {
if info.Soft {
return " โ "
}
if info.Index >= info.TotalLines {
return " ~ โ "
}
return fmt.Sprintf("%4d โ ", info.Index+1)
}Highlight parts of what's being viewed with regex:
vp.SetHighlights(regexp.MustCompile("hello").FindAllStringIndex(vp.GetContent(), -1))
vp.HighlightNext() // highlight and navigate to next match
vp.HighlightPrevious() // highlight and navigate to previous match
vp.ClearHighlights() // clear all highlightsLet viewport handle soft wrapping for you:
vp.SoftWrap = true
vp.SetContent("hello world from a very long line")Or, if you need fine control, use SetContentLines with "virtual lines" containing \n โ they're treated as soft wraps automatically.
Also new:
- Horizontal mouse wheel scrolling (thanks @UnseenBook!)
GetContent()to retrieve contentFillHeightto pad the viewport with empty linesStyleLineFuncfor per-line stylingHighlightStyleandSelectedHighlightStylefor highlight appearance
Changelog
Fixed
- f744b92: fix(ci): use local golangci-lint config (@aymanbagabas)
- 251e612: fix(filepicker): fix a panic due to an unchecked assertion (#891) (@meowgorithm)
- f3f0ca0: fix(lint): exclude var-naming rule for revive (@aymanbagabas)
- d004225: fix(table): use
ansi.Truncateinstead ofrunewidth.Truncate(#884) (@jedevc) - 93a004a: fix(viewport): optimize subline splitting by skipping lines without line endings (@aymanbagabas)
- d016636: fix: changed 'recieve' to 'receive' for 100% quality of Go Report Card (#881) (@Atennop1)
- af98365: fix: lint issues (@aymanbagabas)
Docs
- c81d525: docs(readme): update for v2 (#888) (@aymanbagabas)
- 6a799f4: docs(readme): update header image, minor corrections (@meowgorithm)
- 24081b3: docs: add v2 upgrade and changes guide (#885) (@aymanbagabas)
- 3a5ea3e: docs: update mascot image (@aymanbagabas)
Other stuff
๐ That's a wrap!
Feel free to reach out, ask questions, give feedback, and let us know how it's going. We'd love to know what you think.
Part of Charm.
Charm็ญ็ฑๅผๆบ โข Charm loves open source โข ูุญูู ูุญุจ ุงูู ุตุงุฏุฑ ุงูู ูุชูุญุฉ
