Skip to content

Latest commit

Β 

History

History
610 lines (473 loc) Β· 17.5 KB

File metadata and controls

610 lines (473 loc) Β· 17.5 KB

Upgrading to Bubbles v2

This guide covers every breaking change when migrating from Bubbles v1 (github.com/charmbracelet/bubbles) to Bubbles v2 (charm.land/bubbles/v2). It is written for both humans and LLM-assisted migration tools.

Companion upgrades required. Bubbles v2 requires Bubble Tea v2 and Lip Gloss v2. Upgrade all three together:

go get charm.land/bubbletea/v2@latest
go get charm.land/bubbles/v2@latest
go get charm.land/lipgloss/v2@latest

Table of Contents

  1. Import Paths
  2. Global Patterns
  3. Per-Component Migration
  4. Light and Dark Styles
  5. Removed Symbols Reference

1. Import Paths

Replace all github.com/charmbracelet/bubbles imports with charm.land/bubbles/v2:

// Before
import (
    "github.com/charmbracelet/bubbles/cursor"
    "github.com/charmbracelet/bubbles/help"
    "github.com/charmbracelet/bubbles/key"
    "github.com/charmbracelet/bubbles/list"
    "github.com/charmbracelet/bubbles/paginator"
    "github.com/charmbracelet/bubbles/progress"
    "github.com/charmbracelet/bubbles/runeutil"
    "github.com/charmbracelet/bubbles/spinner"
    "github.com/charmbracelet/bubbles/stopwatch"
    "github.com/charmbracelet/bubbles/table"
    "github.com/charmbracelet/bubbles/textarea"
    "github.com/charmbracelet/bubbles/textinput"
    "github.com/charmbracelet/bubbles/timer"
    "github.com/charmbracelet/bubbles/viewport"
)

// After
import (
    "charm.land/bubbles/v2/cursor"
    "charm.land/bubbles/v2/help"
    "charm.land/bubbles/v2/key"
    "charm.land/bubbles/v2/list"
    "charm.land/bubbles/v2/paginator"
    "charm.land/bubbles/v2/progress"
    "charm.land/bubbles/v2/spinner"
    "charm.land/bubbles/v2/stopwatch"
    "charm.land/bubbles/v2/table"
    "charm.land/bubbles/v2/textarea"
    "charm.land/bubbles/v2/textinput"
    "charm.land/bubbles/v2/timer"
    "charm.land/bubbles/v2/viewport"
)

Note: The runeutil and memoization packages are now internal and no longer importable.

Search-and-replace pattern:

github.com/charmbracelet/bubbles/  β†’  charm.land/bubbles/v2/
github.com/charmbracelet/bubbles   β†’  charm.land/bubbles/v2

2. Global Patterns

These patterns repeat across multiple components. Address them first for the broadest impact.

2a. tea.KeyMsg β†’ tea.KeyPressMsg

Bubble Tea v2 renames tea.KeyMsg to tea.KeyPressMsg. All Bubbles that handle key events have been updated. Update your own Update functions:

// Before
case tea.KeyMsg:

// After
case tea.KeyPressMsg:

2b. Exported Width/Height Fields β†’ Getter/Setter Methods

Many components replaced exported Width and Height fields with methods. The general pattern:

// Before
m.Width = 40
m.Height = 20
fmt.Println(m.Width, m.Height)

// After
m.SetWidth(40)
m.SetHeight(20)
fmt.Println(m.Width(), m.Height())

Affected components: filepicker, help, progress, table, textinput, viewport.

2c. DefaultKeyMap Variables β†’ Functions

Global mutable DefaultKeyMap variables are now functions returning fresh values:

// Before
km := textinput.DefaultKeyMap
km.Paste.SetEnabled(false)

// After
km := textinput.DefaultKeyMap()
km.Paste.SetEnabled(false)

Affected components: paginator, textarea, textinput.

2d. AdaptiveColor β†’ LightDark with isDark bool

Lip Gloss v2 removes AdaptiveColor. Style functions that previously auto-adapted now require an explicit isDark bool parameter. See Section 4 for the full pattern.

2e. Removed NewModel Aliases

All NewModel variables (deprecated aliases for New) have been removed. Use New directly.

Affected components: help, list, paginator, spinner, textinput.


3. Per-Component Migration

Cursor

v1 v2
model.Blink model.IsBlinked
model.BlinkCmd() model.Blink()

Filepicker

v1 v2
DefaultStylesWithRenderer(r) DefaultStyles()
model.Height = 10 model.SetHeight(10)
_ = model.Height _ = model.Height()

Help

v1 v2
model.Width = 80 model.SetWidth(80)
_ = model.Width _ = model.Width()
NewModel() New()

New functions:

  • DefaultStyles(isDark bool) Styles
  • DefaultDarkStyles() Styles
  • DefaultLightStyles() Styles

Apply styles explicitly:

// Before
h := help.New()
// Colors auto-adapted to terminal background

// After
h := help.New()
h.Styles = help.DefaultStyles(isDark)

List

v1 v2
DefaultStyles() DefaultStyles(isDark)
NewDefaultItemStyles() NewDefaultItemStyles(isDark)
styles.FilterPrompt styles.Filter.Focused.Prompt / styles.Filter.Blurred.Prompt
styles.FilterCursor styles.Filter.Cursor
NewModel(...) New(...)

The Styles.FilterPrompt and Styles.FilterCursor fields have been consolidated into Styles.Filter, which is a textinput.Styles struct.

Paginator

v1 v2
DefaultKeyMap (var) DefaultKeyMap() (func)
model.UsePgUpPgDownKeys Removed β€” customize KeyMap directly
model.UseLeftRightKeys Removed β€” customize KeyMap directly
model.UseUpDownKeys Removed β€” customize KeyMap directly
model.UseHLKeys Removed β€” customize KeyMap directly
model.UseJKKeys Removed β€” customize KeyMap directly
NewModel(...) New(...)

Progress

This component has the most extensive changes.

Width

// Before
p.Width = 40
fmt.Println(p.Width)

// After
p.SetWidth(40)
fmt.Println(p.Width())

Colors

Color types changed from string to image/color.Color:

// Before
p.FullColor = "#FF0000"
p.EmptyColor = "#333333"

// After
p.FullColor = lipgloss.Color("#FF0000")
p.EmptyColor = lipgloss.Color("#333333")

Gradient/Blend Options

// Before
progress.New(progress.WithGradient("#5A56E0", "#EE6FF8"))
progress.New(progress.WithDefaultGradient())
progress.New(progress.WithScaledGradient("#5A56E0", "#EE6FF8"))
progress.New(progress.WithDefaultScaledGradient())
progress.New(progress.WithSolidFill("#7571F9"))

// After
progress.New(progress.WithColors(lipgloss.Color("#5A56E0"), lipgloss.Color("#EE6FF8")))
progress.New(progress.WithDefaultBlend())
progress.New(progress.WithColors(lipgloss.Color("#5A56E0"), lipgloss.Color("#EE6FF8")), progress.WithScaled(true))
progress.New(progress.WithDefaultBlend(), progress.WithScaled(true))
progress.New(progress.WithColors(lipgloss.Color("#7571F9")))
v1 v2
WithGradient(a, b string) WithColors(colors ...color.Color)
WithDefaultGradient() WithDefaultBlend()
WithScaledGradient(a, b string) WithColors(...) + WithScaled(true)
WithDefaultScaledGradient() WithDefaultBlend() + WithScaled(true)
WithSolidFill(string) WithColors(color) (single color)
WithColorProfile(termenv.Profile) Removed (automatic)
Update() (tea.Model, tea.Cmd) Update() (Model, tea.Cmd)

New options:

  • WithColorFunc(func(total, current float64) color.Color) β€” dynamic per-cell coloring
  • WithScaled(bool) β€” scale blend to filled portion

Spinner

v1 v2
NewModel() New()
spinner.Tick() (package func) model.Tick() (method)

Stopwatch

// Before
sw := stopwatch.NewWithInterval(500 * time.Millisecond)

// After
sw := stopwatch.New(stopwatch.WithInterval(500 * time.Millisecond))
v1 v2
NewWithInterval(d) New(WithInterval(d))

Table

v1 v2
model.viewport.Width model.Width() / model.SetWidth(w)
model.viewport.Height model.Height() / model.SetHeight(h)

The table already had SetWidth/SetHeight/Width()/Height() in v1, but internally these now use viewport getter/setters.

Textarea

KeyMap

// Before
km := textarea.DefaultKeyMap
// After
km := textarea.DefaultKeyMap()

New key bindings added: PageUp, PageDown.

Styles

The styling system has been restructured:

// Before
ta := textarea.New()
ta.FocusedStyle.Base = lipgloss.NewStyle().Border(lipgloss.RoundedBorder())
ta.BlurredStyle.Base = lipgloss.NewStyle().Border(lipgloss.HiddenBorder())

// After
ta := textarea.New()
// Styles are now nested under a Styles struct
// Access via Styles.Focused and Styles.Blurred (type StyleState)
v1 v2
textarea.Style (type) textarea.StyleState (type)
model.FocusedStyle model.Styles.Focused
model.BlurredStyle model.Styles.Blurred
DefaultStyles() (focused, blurred Style) DefaultStyles(isDark bool) Styles

Cursor

// Before
ta.Cursor                           // cursor.Model (virtual cursor)
ta.SetCursor(col)                   // set cursor column

// After
ta.Cursor()                         // func() *tea.Cursor (real cursor)
ta.SetCursorColumn(col)             // renamed for clarity
ta.VirtualCursor                    // bool: true = virtual, false = real
ta.Styles.Cursor                    // CursorStyle for cursor appearance

New additions:

  • Column() β€” returns current cursor column (0-indexed)
  • ScrollYOffset() β€” returns vertical scroll offset
  • ScrollPosition() β€” returns scroll position
  • MoveToBeginning() / MoveToEnd() β€” navigate to start/end

Textinput

KeyMap

// Before
km := textinput.DefaultKeyMap
// After
km := textinput.DefaultKeyMap()

Width

// Before
ti.Width = 40
// After
ti.SetWidth(40)

Styles

Individual style fields have moved into a Styles struct:

// Before
ti.PromptStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("63"))
ti.TextStyle = lipgloss.NewStyle()
ti.PlaceholderStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
ti.CompletionStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))

// After
s := textinput.DefaultStyles(isDark)
s.Focused.Prompt = lipgloss.NewStyle().Foreground(lipgloss.Color("63"))
s.Focused.Text = lipgloss.NewStyle()
s.Focused.Placeholder = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
s.Focused.Suggestion = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
ti.SetStyles(s)
v1 Field v2 Location
Model.PromptStyle StyleState.Prompt
Model.TextStyle StyleState.Text
Model.PlaceholderStyle StyleState.Placeholder
Model.CompletionStyle StyleState.Suggestion
Model.CursorStyle Styles.Cursor
Model.Cursor (cursor.Model) Model.Cursor() (func β†’ *tea.Cursor)

New:

  • Model.Styles() / Model.SetStyles(Styles) β€” get/set styles
  • Model.VirtualCursor() / Model.SetVirtualCursor(bool) β€” toggle cursor mode

Timer

// Before
t := timer.NewWithInterval(30*time.Second, 100*time.Millisecond)
t := timer.New(30 * time.Second)

// After
t := timer.New(30*time.Second, timer.WithInterval(100*time.Millisecond))
t := timer.New(30 * time.Second)
v1 v2
NewWithInterval(timeout, interval) New(timeout, WithInterval(interval))

Viewport

This component has the most new features alongside its breaking changes.

Constructor

// Before
vp := viewport.New(80, 24)

// After
vp := viewport.New(viewport.WithWidth(80), viewport.WithHeight(24))
// or
vp := viewport.New()
vp.SetWidth(80)
vp.SetHeight(24)

Width, Height, YOffset

// Before
vp.Width = 80
vp.Height = 24
vp.YOffset = 5
fmt.Println(vp.Width, vp.Height, vp.YOffset)

// After
vp.SetWidth(80)
vp.SetHeight(24)
vp.SetYOffset(5)
fmt.Println(vp.Width(), vp.Height(), vp.YOffset())

Removed

  • HighPerformanceRendering β€” removed entirely (deprecated in Bubble Tea v2)

New Features (non-breaking)

These are additions you can adopt incrementally:

  • Soft wrapping: vp.SoftWrap = true
  • Left gutter for 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)
    }
  • Highlighting:
    vp.SetHighlights(regexp.MustCompile("pattern").FindAllStringIndex(vp.GetContent(), -1))
    vp.HighlightNext()
    vp.HighlightPrevious()
    vp.ClearHighlights()
  • SetContentLines([]string) β€” set lines directly with virtual soft-wrap support
  • GetContent() string β€” retrieve content
  • FillHeight bool β€” fill viewport with empty lines
  • StyleLineFunc func(int) lipgloss.Style β€” per-line styling
  • Horizontal scrolling with left/right arrow keys
  • Horizontal mouse wheel scrolling

4. Light and Dark Styles

Lip Gloss v2 removes AdaptiveColor, so Bubbles no longer auto-detect terminal background. You must explicitly choose light or dark styles.

Recommended: Query via Bubble Tea

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:
        isDark := msg.IsDark()
        m.help.Styles = help.DefaultStyles(isDark)
        m.list.Styles = list.DefaultStyles(isDark)
        // ... apply to other components
    }
    return m, nil
}

This is required when using Wish to detect the client's background.

Quick: Use compat Package

import "charm.land/lipgloss/v2/compat"

var isDark = compat.HasDarkBackground()

func main() {
    h := help.New()
    h.Styles = help.DefaultStyles(isDark)
}

Warning: The compat approach uses blocking I/O outside Bubble Tea's event loop and will not detect remote client backgrounds over SSH.

Manual

h.Styles = help.DefaultDarkStyles()   // force dark
h.Styles = help.DefaultLightStyles()  // force light

5. Removed Symbols Reference

Quick-reference table of all removed symbols and their replacements:

Package Removed Replacement
cursor Model.Blink Model.IsBlinked
cursor Model.BlinkCmd() Model.Blink()
filepicker DefaultStylesWithRenderer(r) DefaultStyles()
filepicker Model.Height (field) Model.SetHeight() / Model.Height()
help NewModel New()
help Model.Width (field) Model.SetWidth() / Model.Width()
list NewModel New()
list DefaultStyles() DefaultStyles(isDark)
list NewDefaultItemStyles() NewDefaultItemStyles(isDark)
list Styles.FilterPrompt Styles.Filter (textinput.Styles)
list Styles.FilterCursor Styles.Filter.Cursor
paginator DefaultKeyMap (var) DefaultKeyMap() (func)
paginator NewModel New()
paginator UsePgUpPgDownKeys etc. Customize KeyMap directly
progress WithGradient(a, b) WithColors(colors...)
progress WithDefaultGradient() WithDefaultBlend()
progress WithScaledGradient(a, b) WithColors(...) + WithScaled(true)
progress WithDefaultScaledGradient() WithDefaultBlend() + WithScaled(true)
progress WithSolidFill(string) WithColors(color)
progress WithColorProfile(p) Removed (automatic)
progress Model.Width (field) Model.SetWidth() / Model.Width()
spinner NewModel New()
spinner Tick() (package func) Model.Tick()
stopwatch NewWithInterval(d) New(WithInterval(d))
table Model.Width (field) Model.SetWidth() / Model.Width()
table Model.Height (field) Model.SetHeight() / Model.Height()
textarea DefaultKeyMap (var) DefaultKeyMap() (func)
textarea Style (type) StyleState (type)
textarea Model.FocusedStyle Model.Styles.Focused
textarea Model.BlurredStyle Model.Styles.Blurred
textarea Model.SetCursor(col) Model.SetCursorColumn(col)
textarea DefaultStyles() DefaultStyles(isDark)
textinput DefaultKeyMap (var) DefaultKeyMap() (func)
textinput NewModel New()
textinput Model.Width (field) Model.SetWidth() / Model.Width()
textinput Model.PromptStyle StyleState.Prompt
textinput Model.TextStyle StyleState.Text
textinput Model.PlaceholderStyle StyleState.Placeholder
textinput Model.CompletionStyle StyleState.Suggestion
textinput Model.CursorStyle Styles.Cursor
textinput Model.Cursor (cursor.Model) Model.Cursor() (func β†’ *tea.Cursor)
timer NewWithInterval(t, i) New(t, WithInterval(i))
viewport New(w, h int) New(...Option)
viewport Model.Width (field) Model.SetWidth() / Model.Width()
viewport Model.Height (field) Model.SetHeight() / Model.Height()
viewport Model.YOffset (field) Model.SetYOffset() / Model.YOffset()
viewport HighPerformanceRendering Removed
runeutil Entire package Moved to internal/runeutil (not importable)

Part of Charm.

The Charm logo