Skip to content

Latest commit

ย 

History

History
573 lines (444 loc) ยท 16.8 KB

File metadata and controls

573 lines (444 loc) ยท 16.8 KB

Bubble Tea v2 Upgrade Guide

This guide covers everything you need to change when upgrading from Bubble Tea v1 to v2. For a tour of all the exciting new features, check out the What's New doc.

Note

We don't take API changes lightly and strive to make the upgrade process as simple as possible. If something feels way off, let us know.

Migration Checklist

Here's the short version โ€” a checklist you can follow top to bottom. Each item links to the relevant section below.

Import Paths

The module path changed to a vanity domain. Lip Gloss moved too.

// Before
import tea "github.com/charmbracelet/bubbletea"
import "github.com/charmbracelet/lipgloss"

// After
import tea "charm.land/bubbletea/v2"
import "charm.land/lipgloss/v2"

The Big Idea: Declarative Views

The single biggest change in v2 is the shift from imperative commands to declarative View fields. In v1, you'd use program options like tea.WithAltScreen() and commands like tea.EnterAltScreen to toggle terminal features on and off. In v2, you just set fields on the tea.View struct in your View() method and Bubble Tea handles the rest.

This means: no more startup option flags, no more toggle commands, no more fighting over state. Just declare what you want and Bubble Tea will make it so.

// v1: imperative โ€” scattered across NewProgram, Init, and Update
p := tea.NewProgram(model{}, tea.WithAltScreen(), tea.WithMouseCellMotion())

// v2: declarative โ€” everything lives in View()
func (m model) View() tea.View {
    v := tea.NewView("Hello!")
    v.AltScreen = true
    v.MouseMode = tea.MouseModeCellMotion
    return v
}

Keep this in mind as you go through the rest of the guide โ€” most of the "removed" things simply moved into View fields.

View Returns a tea.View Now

The View() method no longer returns a string. It returns a tea.View struct.

// Before:
func (m model) View() string {
    return "Hello, world!"
}

// After:
func (m model) View() tea.View {
    return tea.NewView("Hello, world!")
}

You can also use the longer form if you need to set additional fields:

func (m model) View() tea.View {
    var v tea.View
    v.SetContent("Hello, world!")
    v.AltScreen = true
    return v
}

The tea.View struct has fields for everything that used to be controlled by options and commands:

View Field What It Does
Content The rendered string (set via SetContent() or NewView())
AltScreen Enter/exit the alternate screen buffer
MouseMode MouseModeNone, MouseModeCellMotion, or MouseModeAllMotion
ReportFocus Enable focus/blur event reporting
DisableBracketedPasteMode Disable bracketed paste
WindowTitle Set the terminal window title
Cursor Control cursor position, shape, color, and blink
ForegroundColor Set the terminal foreground color
BackgroundColor Set the terminal background color
ProgressBar Show a native terminal progress bar
KeyboardEnhancements Request keyboard enhancement features
OnMouse Intercept mouse messages based on view content

Key Messages

Key messages got a major overhaul. Here's the quick rundown:

tea.KeyMsg is now an interface

In v1, tea.KeyMsg was a struct you'd match on for key presses. In v2, it's an interface that covers both key presses and releases. For most code, you want tea.KeyPressMsg:

// Before:
case tea.KeyMsg:
    switch msg.String() {
    case "q":
        return m, tea.Quit
    }

// After:
case tea.KeyPressMsg:
    switch msg.String() {
    case "q":
        return m, tea.Quit
    }

If you want to handle both presses and releases, use tea.KeyMsg and type-switch inside:

case tea.KeyMsg:
    switch key := msg.(type) {
    case tea.KeyPressMsg:
        // key press
    case tea.KeyReleaseMsg:
        // key release
    }

Key fields changed

v1 v2 Notes
msg.Type msg.Code A rune โ€” can be tea.KeyEnter, 'a', etc.
msg.Runes msg.Text Now a string, not []rune
msg.Alt msg.Mod msg.Mod.Contains(tea.ModAlt) for alt, etc.
tea.KeyRune โ€” Check len(msg.Text) > 0 instead
tea.KeyCtrlC โ€” Use msg.String() == "ctrl+c" or check msg.Code + msg.Mod

Space bar changed

Space bar now returns "space" instead of " " when using msg.String():

// Before:
case " ":

// After:
case "space":

key.Code is still ' ' and key.Text is still " ", but String() returns "space".

Ctrl+key matching

// Before:
case tea.KeyCtrlC:
    // ctrl+c

// After (option A โ€” string matching):
case tea.KeyPressMsg:
    switch msg.String() {
    case "ctrl+c":
        // ctrl+c
    }

// After (option B โ€” field matching):
case tea.KeyPressMsg:
    if msg.Code == 'c' && msg.Mod == tea.ModCtrl {
        // ctrl+c
    }

New Key fields

These are new in v2 and don't have v1 equivalents:

  • key.ShiftedCode โ€” the shifted key code (e.g., 'B' when pressing shift+b)
  • key.BaseCode โ€” the key on a US PC-101 layout (handy for international keyboards)
  • key.IsRepeat โ€” whether the key is auto-repeating (Kitty protocol / Windows Console only)
  • key.Keystroke() โ€” like String() but always includes modifier info

Paste Messages

Paste events no longer come in as tea.KeyMsg with a Paste flag. They're now their own message types:

// Before:
case tea.KeyMsg:
    if msg.Paste {
        m.text += string(msg.Runes)
    }

// After:
case tea.PasteMsg:
    m.text += msg.Content
case tea.PasteStartMsg:
    // paste started
case tea.PasteEndMsg:
    // paste ended

Mouse Messages

tea.MouseMsg is now an interface

In v1, tea.MouseMsg was a struct with X, Y, Button, etc. In v2, it's an interface. You get the coordinates by calling msg.Mouse():

// Before:
case tea.MouseMsg:
    x, y := msg.X, msg.Y

// After:
case tea.MouseMsg:
    mouse := msg.Mouse()
    x, y := mouse.X, mouse.Y

Mouse events are split by type

Instead of checking msg.Action, match on specific message types:

// Before:
case tea.MouseMsg:
    if msg.Action == tea.MouseActionPress && msg.Button == tea.MouseButtonLeft {
        // left click
    }

// After:
case tea.MouseClickMsg:
    if msg.Button == tea.MouseLeft {
        // left click
    }
case tea.MouseReleaseMsg:
    // release
case tea.MouseWheelMsg:
    // scroll
case tea.MouseMotionMsg:
    // movement

Button constants renamed

v1 v2
tea.MouseButtonLeft tea.MouseLeft
tea.MouseButtonRight tea.MouseRight
tea.MouseButtonMiddle tea.MouseMiddle
tea.MouseButtonWheelUp tea.MouseWheelUp
tea.MouseButtonWheelDown tea.MouseWheelDown
tea.MouseButtonWheelLeft tea.MouseWheelLeft
tea.MouseButtonWheelRight tea.MouseWheelRight

tea.MouseEvent โ†’ tea.Mouse

The MouseEvent struct is gone. The new Mouse struct has X, Y, Button, and Mod fields.

Mouse mode is now a View field

// Before:
p := tea.NewProgram(model{}, tea.WithMouseCellMotion())

// After:
func (m model) View() tea.View {
    v := tea.NewView("...")
    v.MouseMode = tea.MouseModeCellMotion
    return v
}

Removed Program Options

These options no longer exist. They all moved to View fields.

Removed Option Do This Instead
tea.WithAltScreen() view.AltScreen = true
tea.WithMouseCellMotion() view.MouseMode = tea.MouseModeCellMotion
tea.WithMouseAllMotion() view.MouseMode = tea.MouseModeAllMotion
tea.WithReportFocus() view.ReportFocus = true
tea.WithoutBracketedPaste() view.DisableBracketedPasteMode = true
tea.WithInputTTY() Just remove it โ€” v2 always opens the TTY for input automatically
tea.WithANSICompressor() Just remove it โ€” the new renderer handles optimization automatically

Removed Commands

These commands no longer exist. Set the corresponding View field instead.

Removed Command Do This Instead
tea.EnterAltScreen view.AltScreen = true
tea.ExitAltScreen view.AltScreen = false
tea.EnableMouseCellMotion view.MouseMode = tea.MouseModeCellMotion
tea.EnableMouseAllMotion view.MouseMode = tea.MouseModeAllMotion
tea.DisableMouse view.MouseMode = tea.MouseModeNone
tea.HideCursor view.Cursor = nil
tea.ShowCursor view.Cursor = &tea.Cursor{...} or tea.NewCursor(x, y)
tea.EnableBracketedPaste view.DisableBracketedPasteMode = false
tea.DisableBracketedPaste view.DisableBracketedPasteMode = true
tea.EnableReportFocus view.ReportFocus = true
tea.DisableReportFocus view.ReportFocus = false
tea.SetWindowTitle("...") view.WindowTitle = "..."

Removed Program Methods

These methods on *Program are gone.

Removed Method Do This Instead
p.Start() p.Run()
p.StartReturningModel() p.Run()
p.EnterAltScreen() view.AltScreen = true in View()
p.ExitAltScreen() view.AltScreen = false in View()
p.EnableMouseCellMotion() view.MouseMode in View()
p.DisableMouseCellMotion() view.MouseMode = tea.MouseModeNone in View()
p.EnableMouseAllMotion() view.MouseMode in View()
p.DisableMouseAllMotion() view.MouseMode = tea.MouseModeNone in View()
p.SetWindowTitle(...) view.WindowTitle in View()

Renamed APIs

v1 v2 Notes
tea.Sequentially(...) tea.Sequence(...) Sequentially was already deprecated in v1
tea.WindowSize() tea.RequestWindowSize Now returns Msg directly, not a Cmd

New Program Options

These are new in v2:

Option What It Does
tea.WithColorProfile(p) Force a specific color profile (great for testing)
tea.WithWindowSize(w, h) Set initial terminal size (great for testing)

Complete Before & After

Here's a minimal but complete program showing the most common migration patterns side by side.

v1:

package main

import (
    "fmt"
    "os"

    tea "github.com/charmbracelet/bubbletea"
)

type model struct {
    count int
}

func (m model) Init() tea.Cmd {
    return nil
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.KeyMsg:
        switch msg.String() {
        case "q", "ctrl+c":
            return m, tea.Quit
        case " ":
            m.count++
        }
    case tea.MouseMsg:
        if msg.Action == tea.MouseActionPress && msg.Button == tea.MouseButtonLeft {
            m.count++
        }
    }
    return m, nil
}

func (m model) View() string {
    return fmt.Sprintf("Count: %d\n\nSpace or click to increment. q to quit.\n", m.count)
}

func main() {
    p := tea.NewProgram(model{}, tea.WithAltScreen(), tea.WithMouseCellMotion())
    if _, err := p.Run(); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}

v2:

package main

import (
    "fmt"
    "os"

    tea "charm.land/bubbletea/v2"
)

type model struct {
    count int
}

func (m model) Init() tea.Cmd {
    return nil
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.KeyPressMsg:
        switch msg.String() {
        case "q", "ctrl+c":
            return m, tea.Quit
        case "space":
            m.count++
        }
    case tea.MouseClickMsg:
        if msg.Button == tea.MouseLeft {
            m.count++
        }
    }
    return m, nil
}

func (m model) View() tea.View {
    v := tea.NewView(fmt.Sprintf("Count: %d\n\nSpace or click to increment. q to quit.\n", m.count))
    v.AltScreen = true
    v.MouseMode = tea.MouseModeCellMotion
    return v
}

func main() {
    p := tea.NewProgram(model{})
    if _, err := p.Run(); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}

Notice how the NewProgram call got simpler? All the terminal feature flags moved into View() where they belong.

Quick Reference

A flat old โ†’ new lookup table. Handy for search-and-replace and LLM-assisted migration.

Import Paths

v1 v2
github.com/charmbracelet/bubbletea charm.land/bubbletea/v2
github.com/charmbracelet/lipgloss charm.land/lipgloss/v2

Model Interface

v1 v2
View() string View() tea.View

Key Events

v1 v2
tea.KeyMsg (struct) tea.KeyPressMsg for presses, tea.KeyMsg (interface) for both
msg.Type msg.Code
msg.Runes msg.Text (string, not []rune)
msg.Alt msg.Mod.Contains(tea.ModAlt)
tea.KeyRune check len(msg.Text) > 0
tea.KeyCtrlC msg.Code == 'c' && msg.Mod == tea.ModCtrl or msg.String() == "ctrl+c"
case " ": (space) case "space":

Mouse Events

v1 v2
tea.MouseMsg (struct) tea.MouseMsg (interface) โ€” call .Mouse() for the data
tea.MouseEvent tea.Mouse
tea.MouseButtonLeft tea.MouseLeft
tea.MouseButtonRight tea.MouseRight
tea.MouseButtonMiddle tea.MouseMiddle
tea.MouseButtonWheelUp tea.MouseWheelUp
tea.MouseButtonWheelDown tea.MouseWheelDown
msg.X, msg.Y (direct) msg.Mouse().X, msg.Mouse().Y

Options โ†’ View Fields

v1 Option v2 View Field
tea.WithAltScreen() view.AltScreen = true
tea.WithMouseCellMotion() view.MouseMode = tea.MouseModeCellMotion
tea.WithMouseAllMotion() view.MouseMode = tea.MouseModeAllMotion
tea.WithReportFocus() view.ReportFocus = true
tea.WithoutBracketedPaste() view.DisableBracketedPasteMode = true

Commands โ†’ View Fields

v1 Command v2 View Field
tea.EnterAltScreen / tea.ExitAltScreen view.AltScreen = true/false
tea.EnableMouseCellMotion view.MouseMode = tea.MouseModeCellMotion
tea.EnableMouseAllMotion view.MouseMode = tea.MouseModeAllMotion
tea.DisableMouse view.MouseMode = tea.MouseModeNone
tea.HideCursor / tea.ShowCursor view.Cursor = nil / view.Cursor = &tea.Cursor{...}
tea.EnableBracketedPaste / tea.DisableBracketedPaste view.DisableBracketedPasteMode = false/true
tea.EnableReportFocus / tea.DisableReportFocus view.ReportFocus = true/false
tea.SetWindowTitle("...") view.WindowTitle = "..."

Removed Options (No Replacement Needed)

v1 Option What Happened
tea.WithInputTTY() v2 always opens the TTY for input automatically
tea.WithANSICompressor() The new renderer handles optimization automatically

Removed Program Methods

v1 Method v2 Replacement
p.Start() p.Run()
p.StartReturningModel() p.Run()
p.EnterAltScreen() view.AltScreen = true in View()
p.ExitAltScreen() view.AltScreen = false in View()
p.EnableMouseCellMotion() view.MouseMode in View()
p.DisableMouseCellMotion() view.MouseMode = tea.MouseModeNone in View()
p.EnableMouseAllMotion() view.MouseMode in View()
p.DisableMouseAllMotion() view.MouseMode = tea.MouseModeNone in View()
p.SetWindowTitle(...) view.WindowTitle in View()

Other Renames

v1 v2
tea.Sequentially(...) tea.Sequence(...)
tea.WindowSize() tea.RequestWindowSize (now returns Msg, not Cmd)

New Program Options

Option Description
tea.WithColorProfile(p) Force a specific color profile
tea.WithWindowSize(w, h) Set initial window size (great for testing)

Feedback

Have thoughts on the v2 upgrade? We'd love to hear about it. Let us know onโ€ฆ


Part of Charm.

The Charm logo

Charm็ƒญ็ˆฑๅผ€ๆบ โ€ข Charm loves open source โ€ข ู†ุญู†ู ู†ุญุจ ุงู„ู…ุตุงุฏุฑ ุงู„ู…ูุชูˆุญุฉ