Skip to content

fix(deps): update go dependencies to v2 (major)#14

Open
renovate[bot] wants to merge 1 commit intomainfrom
renovate/major-go-deps
Open

fix(deps): update go dependencies to v2 (major)#14
renovate[bot] wants to merge 1 commit intomainfrom
renovate/major-go-deps

Conversation

@renovate
Copy link
Copy Markdown
Contributor

@renovate renovate bot commented Feb 16, 2026

ℹ️ Note

This PR body was truncated due to platform limits.

This PR contains the following updates:

Package Change Age Confidence
github.com/charmbracelet/bubbles v0.21.1v2.1.0 age confidence
github.com/charmbracelet/bubbletea v1.3.10v2.0.2 age confidence
github.com/charmbracelet/lipgloss v1.1.0v2.0.2 age confidence

Release Notes

charmbracelet/bubbles (github.com/charmbracelet/bubbles)

v2.1.0

Compare Source

Shrink ’n’ grow your textareas

The update adds a new feature to automatically resize your textarea vertically as its content changes.

ta := textarea.New()
ta.DynamicHeight = true   // Enable dynamic resizing
ta.MinHeight = 3          // Minimum visible rows
ta.MaxHeight = 10         // Maximum visible rows
ta.MaxContentHeight = 20  // Maximum rows of content

Piece of cake, right?

Enjoy! 💘

Changelog

New!

The Charm logo

Thoughts? Questions? We love hearing from you. Feel free to reach out on X, Discord, Slack, The Fediverse, Bluesky.

v2.0.0

Compare Source

bubbles-v2-block

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/v2

You 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 mode

This 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.DefaultKeyMap
Real 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:

  • NewModel variants — just use New
  • spinner.Tick() — use Model.Tick() instead
  • paginator.UsePgUpPgDownKeys and friends — customize KeyMap directly
  • filepicker.DefaultStylesWithRenderer() — Lip Gloss is pure now, use DefaultStyles()
  • viewport.HighPerformanceRendering — no longer needed
  • runeutil and memoization packages moved to internal/ (they were never meant for public use anyway)

What's Changed: the Laundry List

🔮 Cursor
  • Model.Blink renamed to Model.IsBlinked for clarity
  • Model.BlinkCmd() renamed to Model.Blink()
  • Each cursor now gets a unique ID
📂 Filepicker
  • DefaultStylesWithRenderer() removed — Lip Gloss is pure now, so just use DefaultStyles()
  • Model.Height broken into SetHeight(int) / Height() int
❓ Help
  • Model.Width broken into SetWidth(int) / Width() int
  • New DefaultStyles(isDark bool), DefaultDarkStyles(), and DefaultLightStyles()
  • Defaults to dark background styles out of the box
🥕 List
  • DefaultStyles() and NewDefaultItemStyles() now take an isDark bool parameter
  • Styles.FilterPrompt and Styles.FilterCursor have been consolidated into Styles.Filter (a textinput.Styles)
  • GlobalIndex helper added
📄 Paginator
  • DefaultKeyMap variable → DefaultKeyMap() function
  • Deprecated fields (UsePgUpPgDownKeys, UseLeftRightKeys, etc.) removed — customize KeyMap directly
🌈 Progress

This one got the biggest makeover!

  • Complete color API overhaul:
    • WithGradient / WithScaledGradientWithColors(...color.Color) — pass 2+ colors for blending
    • WithSolidFill(string)WithColors(color) — pass a single color for a solid fill
    • WithDefaultGradient()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
    • WithColorProfile removed — Bubble Tea handles this automatically now
  • Model.FullColor and Model.EmptyColor changed from string to image/color.Color
  • Model.Width broken into SetWidth(int) / Width() int
  • Model.Update now returns Model instead of tea.Model
  • Improved blend algorithm with support for multiple color stops — special thanks to the legendary @​lrstanley!
🌀 Spinner
  • Tick() package-level function removed — use Model.Tick() instead
⏱️ Stopwatch
  • NewWithInterval(d) removed — use New(WithInterval(d)) instead
  • Debounced tick messages
🔢 Table
  • Model.Width / Model.Height replaced with getter/setter methods
  • Uses ansi.Truncate instead of runewidth.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.

  • DefaultKeyMap variable → DefaultKeyMap() function
  • New PageUp / PageDown key bindings
  • Model.FocusedStyle / Model.BlurredStyleModel.Styles.Focused / Model.Styles.Blurred
  • Style type renamed to StyleState; new Styles struct groups Focused, Blurred, and Cursor
  • Model.SetCursor renamed to Model.SetCursorColumn
  • Model.Cursor is now func() *tea.Cursor for real cursor support
  • Model.VirtualCursor bool added — set to false when 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:

  • DefaultKeyMap variable → DefaultKeyMap() function
  • Model.Width broken into SetWidth(int) / Width() int
  • Model.PromptStyleStyleState.Prompt
  • Model.TextStyleStyleState.Text
  • Model.PlaceholderStyleStyleState.Placeholder
  • Model.CompletionStyleStyleState.Suggestion
  • Model.Cursor is now func() *tea.Cursor for real cursor support
  • Model.VirtualCursor() / SetVirtualCursor(bool) added
  • Model.Styles() / SetStyles(Styles) added
  • New DefaultStyles(isDark bool), DefaultDarkStyles(), DefaultLightStyles()
  • Exposed matched suggestions and suggestion index
⏲️ Timer
  • NewWithInterval(timeout, interval) removed — use New(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) with WithWidth / WithHeight
  • Model.Width, Model.Height, Model.YOffset replaced with getter/setter methods
  • HighPerformanceRendering removed

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 highlights

Let 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 content
  • FillHeight to pad the viewport with empty lines
  • StyleLineFunc for per-line styling
  • HighlightStyle and SelectedHighlightStyle for highlight appearance

Changelog

Fixed
  • f744b929dddecc7863cf78605c5bfc396d90abc3: fix(ci): use local golangci-lint config (@​aymanbagabas)
  • 251e612949595b006e0e4739029d45e32c6b34b6: fix(filepicker): fix a panic due to an unchecked assertion (#​891) (@​meowgorithm)
  • f3f0ca0fe2f05b56e5a0c69b226b4d752c5e8f4a: fix(lint): exclude var-naming rule for revive (@​aymanbagabas)
  • d004225e8c3b8c8ddb14a76a5101728d666396f3: fix(table): use ansi.Truncate instead of runewidth.Truncate (#​884) (@​jedevc)
  • 93a004ab70c8ea979940b2720b3993c8f68bf8dc: fix(viewport): optimize subline splitting by skipping lines without line endings (@​aymanbagabas)
  • d0166363eb8176b331de98dba1d6e997560f216f: fix: changed 'recieve' to 'receive' for 100% quality of Go Report Card (#​881) (@​Atennop1)
  • af98365cc63af118d838e05522f8dddf16ad827e: fix: lint issues (@​aymanbagabas)
Docs
  • c81d525337e1a059c4343cf65a02eea020470a48: docs(readme): update for v2 (#​888) (@​aymanbagabas)
  • 6a799f4d58cc0eaeab0874f4ce9c98b5a922bd01: docs(readme): update header image, minor corrections (@​meowgorithm)
  • 24081b3590e746db4efa2ec09e31a85e2c078427: docs: add v2 upgrade and changes guide (#​885) (@​aymanbagabas)
  • 3a5ea3e2eb42aa064bb4a0ffe3262cb2b8a1f19b: docs: update mascot image (@​aymanbagabas)
Other stuff
  • ae99f46cec66f45862c2d953bb1af31efdc4f073: feat(v2/textarea): expose Column(), clarify 0-indexing (#​875) (@​caarlos0)

💝 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.

The Charm logo

Charm热爱开源 • Charm loves open source • نحنُ نحب المصادر المفتوحة

v1.0.0

Compare Source

This is just an honorary release of Bubbles v1. Stay tuned for the next major version 🫧

Changelog

Fixed

The Charm logo

Thoughts? Questions? We love hearing from you. Feel free to reach out on X, Discord, Slack, The Fediverse, Bluesky.

charmbracelet/bubbletea (github.com/charmbracelet/bubbletea)

v2.0.2

Compare Source

This release contains a small patch fixing a rendering that might affect Wish users running on Unix platforms.

Changelog

Fixed

The Charm logo

Thoughts? Questions? We love hearing from you. Feel free to reach out on X, Discord, Slack, The Fediverse, Bluesky.

v2.0.1

Compare Source

A small patch release to fix opening the proper default stdin file for input.

Changelog

Fixed
Docs

The Charm logo

Thoughts? Questions? We love hearing from you. Feel free to reach out on X, Discord, Slack, The Fediverse, Bluesky.

v2.0.0

Compare Source

bubbletea-v2-block

What's New in Bubble Tea v2

We're very excited to announce the second major release of Bubble Tea!

If you (or your LLM) are just looking for technical details on on migrating from v1, please check out the Upgrade Guide.

[!NOTE]
We don't take API changes lightly and strive to make the upgrade process as simple as possible. We believe the changes bring necessary improvements as well as pave the way for the future. If something feels way off, let us know.

❤️ Charm Land Import Path

We've updated our import paths to use vanity domains and use our domain to import Go packages.

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

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

Everything else stays the same 🙂

👾 The Cursed Renderer

Bubble Tea v2 ships with the all-new Cursed Renderer which was built from the ground up. It's based on the ncurses rendering algorithm and is highly optimized for speed, efficiency, and accuracy and is built on an enormous amount of research and development.

Optimized renders also means that Wish users get big performance benefits and lower bandwidth usage by orders of magnitude.

To take advantage of the new Cursed Renderer you don't need to do anything at all except keep on using the Bubble Tea you know and love.

✌️ Key handling is way better now

Newer terminals can now take advantage of all sorts keyboard input via progressive keyboard enhancements. You can now map all sorts of keys and modifiers like shift+enter and super+space. You can also detect key releases (we're looking at you, game developers).

It's easy to detect support for supporting terminals and add fallbacks for those that don't. For details, see keyboard enhancements below.

🥊 No more fighting

In the past, Bubble Tea and Lip Gloss would often fight over i/o. Bubble Tea wanted to read keyboard input and Lip Gloss wanted to query for the background color. This means that things could get messy. Not anymore! In v2, Lip Gloss is now pure, which means, Bubble Tea manages i/o and gives orders to Lip Gloss. In short, we only need one lib to call the shots, and in the context of this relationship, that lib is Bubble Tea.

But what about color downsampling? That's a great question.

👨🏻‍🎨 Built-in Color Downsampling

We sneakily released a little library called colorprofile that will detect the terminal's color profile and auto-downsample any ANSI styling that flows through it to the best available color profile. This means that color will "just work" (and not misbehave) no matter where the ANSI styling comes from.

Downsampling is built-into Bubble Tea and is automatically enabled.

🧘 Declarative, Not Imperative

This is a big one. In v1, you'd toggle terminal features on and off with commands like tea.EnterAltScreen, tea.EnableMouseCellMotion, tea.EnableReportFocus, and so on. In v2, all of that is gone and replaced by fields on the View struct. You just declare what you want your view to look like and Bubble Tea takes care of the rest.

This means no more fighting over startup options and commands. Just set the fields and forget about it. For example, to enter full screen mode:

func (m Model) View() tea.View {
    v := tea.NewView("Hello, full screen!")
    v.AltScreen = true
    return v
}

The same goes for mouse mode, bracketed paste, focus reporting, window title, keyboard enhancements, and more. See A Declarative View below for the full picture.

Keyboard Enhancements

Progressive keyboard enhancements allow you to receive key events not normally possible in traditional terminals. For example, you can now listen for the ctrl+m key, as well as previously unavailable key combinations like shift+enter.

Bubble Tea v2 will always try to enable basic keyboard enhancements that disambiguate keys. If your terminal supports it, your program will receive a tea.KeyboardEnhancementsMsg message that indicates support for requested features.

func (m Model) View() tea.View {
    var v tea.View
    // ...
    v.KeyboardEnhancements.ReportEventTypes = true           // Enable key release events
    return v
}

Historically, certain key combinations in terminals map to control codes. For example, ctrl+h outputs a backspace by default, which means you can't normally bind a key event to ctrl+h. With key disambiguation, you can now actually bind events to those key combinations.

You can detect if a terminal supports keyboard enhancements by listening for tea.KeyboardEnhancementsMsg.

func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.KeyboardEnhancementsMsg:
        if msg.SupportsKeyDisambiguation() {
            // More keys, please!
        }
    }
}
Which terminals support progressive enhancement?

Key Messages

Key messages are now split into tea.KeyPressMsg and tea.KeyReleaseMsg. Use tea.KeyMsg to match against both. We've also replaced key.Type and key.Runes with key.Code and key.Text. Modifiers live in key.Mod now instead of being separate booleans. Oh, and space bar returns "space" instead of " ".

The easiest way to match against key press events is to use msg.String():

func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.KeyPressMsg:
        switch msg.String() {
        case "space":
            // Space bar returns "space" now :D
            return m, tea.Println("You pressed the space bar!")
        case "ctrl+c":
            return m, tea.SetClipboard("Howdy")
        case "shift+enter":
            // Awesome, right?
        case "ctrl+alt+super+enter":
            // Yes, you can do that now!
        }
    }
}

The Key struct also has some nice new fields:

  • key.BaseCode — the key according to a standard US PC-101 layout. Handy for international keyboards where the physical key might differ.
  • key.IsRepeat — tells you if the key is being held down and auto-repeating. Only available with the Kitty Keyboard Protocol or Windows Console API.
  • key.Keystroke() — a new method that returns the keystroke representation (e.g., "ctrl+shift+alt+a"). Unlike String(), it always includes modifier info.

For the full list of changes and before/after code samples, see the Upgrade Guide.

Paste Messages

Paste events used to arrive as tea.KeyMsg with a confusing msg.Paste flag. Now they're their own thing:

func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.PasteMsg:
        // Here comes a paste!
        m.text += msg.Content
    case tea.PasteStartMsg:
        // The user started pasting.
    case tea.PasteEndMsg:
        // The user stopped pasting.
    }
}

Mouse Messages

We've improved the mouse API. Mouse messages are now split into tea.MouseClickMsg, tea.MouseReleaseMsg, tea.MouseWheelMsg, and tea.MouseMotionMsg. And mouse mode is set declaratively in your View():

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.MouseClickMsg:
        if msg.Button == tea.MouseLeft {
            // Clickety click
        }
    case tea.MouseWheelMsg:
        // Scroll, scroll, scrollllll
    }
    return m, nil
}

func (m model) View() tea.View {
    v := tea.NewView("Move that mouse around!")
    v.MouseMode = tea.MouseModeAllMotion // or tea.MouseModeCellMotion
    return v
}

A Declarative View

In v1, View() returned a string. In v2, it returns a tea.View struct that lets you declare everything about your view — content, cursor, alt screen, mouse mode, colors, window title, progress bar, and more:

type View struct {
	Content                   string
	OnMouse                   func(msg MouseMsg) Cmd
	Cursor                    *Cursor
	BackgroundColor           color.Color
	ForegroundColor           color.Color
	WindowTitle               string
	ProgressBar               *ProgressBar
	AltScreen                 bool
	ReportFocus               bool
	DisableBracketedPasteMode bool
	MouseMode                 MouseMode
	KeyboardEnhancements      KeyboardEnhancements
}

No more fighting over options and commands! Just set the fields:

func (m Model) View() tea.View {
  v := tea.NewView(fmt.Sprintf("Hello, world!"))
  v.AltScreen = true
  v.MouseMode = tea.MouseModeCellMotion
  v.ReportFocus = true
  v.WindowTitle = "My Awesome App"
  return v
}

An Actual Cursor

You can now control the cursor position, color, and shape right from your view function. Want it hidden? Just set view.Cursor = nil.

func (m Model) View() tea.View {
	var v tea.View
	if m.showCursor {
		v.Cursor = &tea.Cursor{
			Position: tea.Position{
				X: 14, // At the 14th column
				Y: 0,  // On the first row
			},
			Shape: tea.CursorBlock, // Just give me a block cursor '█'
			Blink: true,            // Blink baby, blink!
			Color: lipgloss.Green,  // Green cursor, because why not?
		}
	}
	v.SetContent(fmt.Sprintf("Hello, world!"))
	return v
}

You can also use tea.NewCursor(x, y) for a quick block cursor with default settings.

Progress Bar Support

Now you can ask Bubble Tea to render a native progress bar for your application. Just set the view.ProgressBar field and Bubble Tea will take care of the rest.

func (m Model) View() tea.View {
    var v tea.View
    v.SetContent("Downloading...")
    v.ProgressBar = tea.NewProgressBar(tea.ProgressBarDefault, m.downloadProgress)
    return v
}

Synchronized Updates (Mode 2026)

Bubble Tea will try and use mode 2026 to push updates to the terminal. This mode helps reduce tearing and cursor flickering by atomically updating the terminal window once all the update sequences are pushed out and read by the terminal. This is enabled by default and there's nothing you need to do.

Better Terminal Unicode Support (mode 2027)

Now Bubble Tea will automatically enable mode 2027
on terminals that support it. This mode allows the terminal to properly handle wide Unicode
characters and emojis without breaking the layout of your app. Again, this is
enabled by default and there's nothing you need to do.

Native Clipboard Support

Bubble Tea now supports native clipboard operations, also known as OSC52. This means you can even copy and paste over SSH!

func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.KeyPressMsg:
        switch msg.String() {
        case "ctrl+c":
            return m, tea.SetClipboard("Howdy")
        case "ctrl+v":
            return m, tea.ReadClipboard()
        }
    case tea.ClipboardMsg:
        fmt.Printf("Clipboard contents: %s\n", msg.String())
    }
}

X11 and Wayland users can also use tea.SetPrimaryClipboard to set the primary clipboard. Note that this is a very niche sort of thing and may or may not work on macOS, Windows, and other platforms without the notion of more than one clipboard.

Terminal Colors

You can now read and set the terminal's foreground, background, and cursor colors. To change them, set view.ForegroundColor, view.BackgroundColor, and view.Cursor.Color in your View() function.

func (m Model) Init() tea.Cmd {
    return tea.Batch(
        tea.RequestForegroundColor,
        tea.RequestBackgroundColor,
        tea.RequestCursorColor,
    )
}

func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.BackgroundColorMsg:
        return m, tea.Printf("Background color: %s\n", msg)
    case tea.ForegroundColorMsg:
        return m, tea.Printf("Foreground color: %s\n", msg)
    case tea.CursorColorMsg:
        return m, tea.Printf("Cursor color: %s\n", msg)
    case tea.KeyPressMsg:
        switch msg.String() {
        case "enter":
            m.fg, m.bg, m.cursor = ansi.Red, ansi.Green, ansi.Blue
        case "esc":
            return m, tea.Quit
        }
    }
    return m, nil
}

func (m Model) View() tea.View {
    var v tea.View
    v.SetContent("\nPress Enter to change terminal colors, Esc to quit.")
    v.ForegroundColor = m.fg
    v.BackgroundColor = m.bg
    if m.cursor != nil {
        v.Cursor = tea.NewCursor(0, 1)
        v.Cursor.Color = m.cursor
    }
    return v
}

🌍 Environment Variables

Bubble Tea now sends you a tea.EnvMsg at startup with the environment variables. This is especially handy for SSH apps where os.Getenv would give you the server's environment, not the client's.

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.EnvMsg:
        m.term = msg.Getenv("TERM") // the client's TERM, not the server's!
    }
    return m, nil
}

🔮 Raw Escape Sequences

For the power users out there, you can now send raw escape sequences directly to the terminal with tea.Raw. This is great for querying terminal capabilities or doing things Bubble Tea doesn't have a built-in for (yet).

return m, tea.Raw(ansi.RequestPrimaryDeviceAttributes)

Responses from the terminal will come back as messages in Update. Just be sure you know what you're doing — with great power comes great terminal weirdness.

📍 Cursor Position Queries

Need to know where the cursor is? Now you can ask.

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

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.CursorPositionMsg:
        m.cursorX, m.cursorY = msg.X, msg.Y
    }
    return m, nil
}

📊 Terminal Mode Reports

You can query whether the terminal supports specific modes (like focus events or synchronized output) using DECRPM mode reports. Send a raw DECRQM request and listen for tea.ModeReportMsg.

func (m model) Init() tea.Cmd {
    return tea.Raw(ansi.RequestModeFocusEvent)
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.ModeReportMsg:
        if msg.Mode == ansi.ModeFocusEvent && !msg.Value.IsNotRecognized() {
            m.supportsFocus = true
        }
    }
    return m, nil
}

Terminal Version and Name

Don't know what terminal you're running in? $TERM is too vague? Bubble Tea now has a tea.RequestTerminalVersion command that queries the terminal for its name and version using the XTVERSION escape sequence.

[!NOTE]
This feature is not supported by all terminals.

func (m Model) Init() tea.Cmd {
    return tea.RequestTerminalVersion
}

func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.TerminalVersionMsg:
        fmt.Printf("Terminal: %s\n", string(msg))
    }
}

Terminfo and Termcap Capabilities

Sometimes you need to know what capabilities the terminal has. Bubble Tea now has a tea.RequestCapability command that queries the terminal for a specific terminfo/termcap capability.

[!NOTE]
This feature is not supported by all terminals.

func (m Model) Init() tea.Cmd {
    return tea.RequestCapability("RGB") // RGB is the terminfo capability for direct colors
}

Detecting the Color Profile

Need to use the detected color profile in your app? Listen to tea.ColorProfileMsg in Update:

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.ColorProfileMsg:
        m.colorProfile = msg.Profile // gottem!
    }
    return m, nil
}

Manually Applying a Color Profile

Want to manually set a color profile for testing? Now you can, on the program level.

import (
    tea "charm.land/bubbletea/v2"
    "github.com/charmbracelet/colorprofile"
)

p := colorprofile.TrueColor // i love colors. lets' use 16,777,216 of 'em
p = colorprofile.ANSI256    // jk, 256 colors are plenty
p = colorprofile.ANSI       // actually let's juse use 16 colors
p = colorprofile.Ascii      // nm, no colors, but keep things like bold, italics, etc.
p = colorprofile.NoTTY      // lol actually strip all ANSI sequences

prog := tea.NewProgram(model, tea.WithColorProfile(p))

Want to hard detect the color profile in Wish? We bet you do.

func main() {
    var s ssh.Session
    pty, _, _ := s.Pty()

    // Get the environment...
    envs := append(s.Environ(), "TERM="+pty.Term)

    // ...and give it to Bubble Tea so it can detect the color profile.
    opt := tea.WithEnvironment(envs)

    p := tea.NewProgram(model,
        tea.WithInput(pty.Slave),
        tea.WithOutput(pty.Slave),
        opt, // wow
    )
}

🪟 Window Size for Testing

When running tests or in non-interactive environments, you can now set the initial terminal size:

p := tea.NewProgram(model, tea.WithWindowSize(80, 24))

No more mocking terminals just to run your tests. Nice!

Use the Terminal's TTY

Sometimes your program will write to stdout while it's being piped or
redirected. In these cases, you might want to write directly to the terminal's
TTY instead of stdout because stdout might not be a terminal. Or your program
expects to read from stdin but stdin is being piped from another program.

In Bubble Tea v1, there wasn't a good way to do this. In the latter case, you
could use the WithInputTTY() option to read from the terminal's TTY instead
of stdin. However, there was no easy way to write to the terminal's TTY instead
of stdout without fiddling with file descriptors.

In Bubble Tea v2, you can now simply use the global OpenTTY() to open the
terminal's TTY for reading and writing. You can then pass the TTY file handles
to the WithInput() and WithOutput() options.

Note that Bubble Tea v2 will always use the TTY for input when input is not specified
via WithInput(...).

ttyIn, ttyOut, err := tea.OpenTTY()
if err != nil {
    log.Fatal(err)
}

p := tea.NewProgram(model,
    tea.WithInput(ttyIn),
    tea.WithOutput(ttyOut),
)

Changelog

New!
  • 742b944f78af9b62e774a98855cb324dfe3313be: feat(render): enable Unicode mode (2027) for accurate width calculation (#​1584) (@​aymanbagabas)
  • 724479d6d5be45005d5a405e3cb6c0b6e8b1fd0e: feat(renderer): add modifyOtherKeys support (#​1579) (@​erikstmartin)
  • 1f48289368a38a670c6cb47929a4e33a2e7d5c1b: feat(renderer): enable both modifyOtherKeys 1 and 2 (@​aymanbagabas)
  • 0b472523eae41c772236ceab8531878f20972e31: feat: add view callback support (@​aymanbagabas)
Fixed
  • 323a3936e4b5f8c08aa638ca2917097c7c58bdeb: fix(ci): use local golangci-lint config (@​aymanbagabas)
  • b65daeb9d46142d2f428303eb77de3d3b65c2956: fix(examples): capability make sure input is focused on start (@​aymanbagabas)
  • 76f2e6d81219acce9d8eccb61845d20a77216cf1: fix(render): always assume raw mode for terminal output (@​aymanbagabas)
  • 61a3f5ccf812adb44eb1d62a2a3032af52fe4330: fix(render): execute insert above immediately (#​1576) (@​aymanbagabas)
  • fdb86513b5e15c6d4a7886221aa2e452738c0e57: fix(renderer): always move cursor to bottom on close (@​aymanbagabas)
  • 819e2e89c62ee26dc01a81546f099e633ab3c704: fix(renderer): flush after moving cursor on close (@​aymanbagabas)
  • fb790535cc28bec18b58c9947d46cb39515a0a74: fix(renderer): make sure we don't skip prepended lines on no-op frames (@​aymanbagabas)
  • 99c33bc3007094d114b7b5e412be03fae71728f8: fix(renderer): move cursor to bottom before disabling alt screen (@​aymanbagabas)
  • 6595041da277654876da2e5458eeed687ba4687c: fix(renderer): no need to enable both modifyOtherKeys protocols (@​aymanbagabas)
  • 59807cf07c8fd071cb252d2319604b22c711dd29: fix(renderer): reset kitty keyboard protocol on alt screen switch (#​1554) (@​aymanbagabas)
  • 2a0096c500a7ee38a5f244f6a80a1d50e6a76014: fix(renderer): restore state when restarting cursed renderer (#​1553) (@​aymanbagabas)
  • 1ff0a470e0d938dc6e8a1b3c659f98a73d042df8: fix(tea): don't query for synchronized output if the renderer is disabled (@​aymanbagabas)
  • ec0d820c3439f58cc92d4461edfc9fd8482f34de: fix(tea): only send actual mouse events to renderer on mouse callback (@​aymanbagabas)
  • b3661ce3d63f003545323b721212562c28647e0f: fix: always open the TTY for input (@​aymanbagabas)
  • ba8f60582376c0b45a93c0277de1949325cbca9c: fix: go.mod and go.sum to use lipgloss v2 (@​aymanbagabas)
  • faa0b9c36ba2d2ed00d8b9e16b8ff61bd7f28e0b: fix: lint issues (@​aymanbagabas)
Docs
  • fd7b0071c582d18d7e059c81d17fda03804ccade: docs(readme): add note about v2 upgrade guide (@​meowgorithm)
  • 07a69fbcf1e3649a797a929d724f94170d6affc3: docs(readme): update bubbles callout image (@​meowgorithm)
  • 3499dac84886f5a54f7ccfc7f3446a361bf2da2e: docs(readme): update header image (@​meowgorithm)
  • ceab368db0d8fe42f8beac3c28bc14f6d95a34d2: docs(readme): updates for v2 (#​1589) (@​aymanbagabas)
  • 59ca08b21ca50c466cba0af717158df20aef5043: docs: add v2 upgrade and changes guide (#​1585) (@​aymanbagabas)
  • 46b608f1528a7a8f51e2ba767c884acfa75c0269: docs: update OnMouse callback to use msg parameter (@​aymanbagabas)
  • 105959b37b4f80600cff83b67187a266cbd5b0be: docs: update mascot header image with a cleaner one (@​aymanbagabas)
  • 5a41615aa4ff78da82ca901b540c3202fa810df3: docs: view cursor api typo (#​1557) (@​bountis)
Other stuff
  • 764923436993564e6e31ae39746e8eaf0e7ad5e6: ci: sync dependabot config (#​1505) (@​charmcli)
  • d1cf96d0d3c9119e422383c2993705fd148cdd86: refactor: limit view callback to onMouse for mouse events (@​aymanbagabas)
  • 14519253d503b2782910f5527f35f08cda5df5de: refactor: omit unnecessary reassignment (#​1515) (@​goldlinker)
  • ece00b4f40c5863709daa28786d0887560811fa5: refactor: remove unused max func (#​1537) (@​sunnyraindy)

🌈 More on Bubble Tea v2

Ready to migrate? Head over to the Upgrade Guide for the full migration checklist.

Feedback

Have thoughts on Bubble Tea v2? We'd love to hear about it. Let us know on…


Part of Charm.

The Charm logo

Charm热爱开源 • Charm loves open source • نحنُ نحب المصادر المفتوحة

charmbracelet/lipgloss (github.com/charmbracelet/lipgloss)

v2.0.2

Compare Source

Table patch

If you don't know, we made big improvements in table rendering recently shipped in v2.0.0.

@​MartinodF made a good job on improving it even further for tricky edge cases, in particular when content wrapping is enabled.

Changelog

Fixed

The Charm logo

Thoughts? Questions? We love hearing from you. Feel free to reach out on X, Discord, Slack, The Fediverse, Bluesky.

v2.0.1

Compare Source

A small release to properly set style underline colors, as well as handling partial reads while querying the terminal.

Changelog

Fixed
Docs
Other stuff

The Charm logo

Thoughts? Questions? We love hearing from you. Feel free to reach out on X, Discord, Slack, The Fediverse, Bluesky.

v2.0.0

Compare Source

lipgloss-v2-block

Do you think you can handle Lip Gloss v2?

We’re really excited for you to try Lip Gloss v2! Read on for new features and a guide to upgrading.

If you (or your LLM) just want the technical details, take a look at Upgrade Guide.

[!NOTE]
We take API changes seriously and strive to make the upgrade process as simple as possible. We believe the changes bring necessary improvements as well as pave the way for the future. If something feels way off, let us know.

What’s new?

The big changes are that Styles are now deterministic (λipgloss!) and you can be much more intentional with your inputs and outputs. Why does this matter?

Playing nicely with others

v2 gives you precise control over I/O. One of the issues we saw with the Lip Gloss and Bubble Tea v1s is that they could fight over the same inputs and outputs, producing lock-ups. The v2s now operate in lockstep.

Querying the right inputs and outputs

In v1, Lip Gloss defaulted to looking at stdin and stdout when downsampling colors and querying for the background color. This was not always necessarily what you wanted. For example, if your application was writing to stderr while redirecting stdout to a file, the program would erroneously think output was not a TTY and strip colors. Lip Gloss v2 gives you control over this.

Going beyond localhost

Did you know TUIs and CLIs can be served over the network? For example, Wish allows you to serve Bubble Tea and Lip Gloss over SSH. In these cases, you need to work with the input and output of the connected clients as opposed to stdin and stdout, which belong to the server. Lip Gloss v2 gives you flexibility around this in a more natural way.

🧋 Using Lip Gloss with Bubble Tea?

Make sure you get all the latest v2s as they’ve been designed to work together.

# Collect the whole set.
go get charm.land/bubbletea/v2
go get charm.land/bubbles/v2
go get charm.land/lipgloss/v2

🐇 Quick upgrade

If you don't have time for changes and just want to upgrade to Lip Gloss v2 as fast as possible? Here’s a quick guide:

Use the compat package

The compat package provides adaptive colors, complete colors, and complete adaptive colors:

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

// Before
color := lipgloss.AdaptiveColor{Light: "#f1f1f1", Dark: "#cccccc"}

// After
color := compat.AdaptiveColor{Light: lipgloss.Color("#f1f1f1"), Dark: lipgloss.Color("#cccccc")}

compat works by looking at stdin and stdout on a global basis. Want to change the inputs and outputs? Knock yourself out:

import (
	"charm.land/lipgloss/v2/compat"
	"github.com/charmbracelet/colorprofile"
)

func init() {
	// Let’s use stderr instead of stdout.
	compat.HasDarkBackground = lipgloss.HasDarkBackground(os.Stdin, os.Stderr)
	compat.Profile = colorprofile.Detect(os.Stderr, os.Environ())
}
Use the new Lip Gloss writer

If you’re using Bubble Tea with Lip Gloss you can skip this step. If you're using Lip Gloss in a standalone fashion, however, you'll want to use lipgloss.Println (and lipgloss.Printf and so on) when printing your output:

s := someStyle.Render("Fancy Lip Gloss Output")

// Before
fmt.Println(s)

// After
lipgloss.Println(s)

Why? Because lipgloss.Println will automatically downsample colors based on the environment.

That’s it!

Yep, you’re done. All this said, we encourage you to read on to get the full benefit of v2.

👀 What’s changing?

Only a couple main things that are changing in Lip Gloss v2:

  • Color downsampling in non-Bubble-Tea uses cases is now a manual proccess (don't worry, it's easy)
  • Background color detection and adaptive colors are manual, and intentional (but optional)
🪄 Downsampling colors with a writer

One of the best things about Lip Gloss is that it can automatically downsample colors to the best available profile, stripping colors (and ANSI) entirely when output is not a TTY.

If you're using Lip Gloss with Bubble Tea there's nothing to do here: downsampling is built into Bubble Tea v2. If you're not using Bubble Tea you now need to use a writer to downsample colors. Lip Gloss writers are a drop-in replacement for the usual functions found in the fmt package:

s := someStyle.Render("Hello!")

// Downsample and print to stdout.
lipgloss.Println(s)

// Render to a variable.
downsampled := lipgloss.Sprint(s)

// Print to stderr.
lipgloss.Fprint(os.Stderr, s)
🌛 Background color detection and adaptive colors

Rendering different colors depending on whether the terminal has a light or dark background is an awesome power. Lip Gloss v2 gives you more control over this progress. This especially matters when input and output are not stdin and stdout.

If that doesn’t matter to you and you're only working with stdout you skip this via compat above, though we encourage you to explore this new functionality.

With Bubble Tea

In Bubble Tea, request the background color, listen for a BackgroundColorMsg in your update, and respond accordingly.

// Query for the background color.
func (m model) Init() tea.Cmd {
	return tea.RequestBackgroundColor
}

// Listen for the response and initialize your styles accordigly.
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.BackgroundColorMsg:
		// Initialize your styles now that you know the background color.
		m.styles = newStyles(msg.IsDark())
		return m, nil
	}
}

type styles {
    myHotStyle lipgloss.Style
}

func newStyles(bgIsDark bool) (s styles) {
	lightDark := lipgloss.LightDark(bgIsDark) // just a helper function
	return styles{
		myHotStyle := lipgloss.NewStyle().Foreground(lightDark("#f1f1f1", "#​333333"))
	}
}
Standalone

If you're not using Bubble Tea you simply can perform the query manually:

// Detect the background color. Notice we're writing to stderr.
hasDarkBG, err := lipgloss.HasDarkBackground(os.Stdin, os.Stderr)
if err != nil {
    log.Fatal("Oof:", err)
}

// Create a helper for choosing the appropriate color.
lightDark := lipgloss.LightDark(hasDarkBG)

// Declare some colors.
thisColor := lightDark("#C5ADF9", "#​864EFF")
thatColor := lightDark("#​37CD96", "#​22C78A")

// Render some styles.
a := lipgloss.NewStyle().Foreground(thisColor).Render("this")
b := lipgloss.NewStyle().Foreground(thatColor).Render("that")

// Print to stderr.
lipgloss.Fprintf(os.Stderr, "my fave colors are %s and %s...for now.", a, b)

🥕 Other stuff

Colors are now color.Color

lipgloss.Color() now produces an idiomatic color.Color, whereas before colors were type lipgloss.TerminalColor. Generally speaking, this is more of an implementation detail, but it’s worth noting the structural differences.

// Before
type TerminalColor interface{/* ... */}
type Color string

// After
func Color(string) color.Color
type RGBColor struct{R, G, B uint8}

func LightDark(isDark bool) LightDarkFunc
type LightDarkFunc func(light, dark color.Color) color.Color
func Complete(colorprofile.Profile) CompleteFunc
type CompleteFunc func(ansi, ansi256, truecolor color.Color) color.Color

Changelog

New!
  • b259725e46e9fbb2af6673d74f26917ed42df370: feat(blending): early return when steps <= num stops (#​566) (@​lrstanley)
  • 71dd8ee66ac1f4312844a792952789102513c9c5: feat(borders): initial border blend implementation (#​560) (@​lrstanley)
  • 2166ce88ec1cca66e8a820a86baafd7cfd34bcd0: feat(canvas): accept any type as layer content (@​aymanbagabas)
  • 0303864674b37235e99bc14cd4da17c409ec448e: feat(colors): refactor colors sub-package into root package (@​lrstanley)
  • 9c86c1f950fbfffd6c56a007de6bd3e61d67a1ea: feat(colors): switch from int to float64 for inputs (@​lrstanley)
  • 0334bb4562ca1f72a684c1c2a63c848ac21fffc6: feat(tree): support width and indenter styling (#​446) (@​dlvhdr)
  • 9a771f5a242df0acf862c7acd72124469eb4635a: feat: BlendLinear* -> Blend* (@​lrstanley)
  • 34443e82a7ddcbe37b9dc0d69b84385e400b8a5c: feat: add brightness example, misc example tweaks (@​lrstanley)
  • c95c5f3c5b27360d344bf82736a8ce9257aaf71e: feat: add hyperlink support (#​473) (@​aymanbagabas)
  • 5e542b8c69a0f20ea62b2caa422bbee5337fbb48: feat: add underline style and color (@​aymanbagabas)
  • d3032608aa74f458a7330e17cc304f1ebb5fa1b9: feat: add wrap implementation preserving styles and links (#​582) (@​aymanbagabas)
  • 7bf18447c8729839ca7e79aa3ba9aa00ecb8f963: feat: further simplify colors in examples (@​lrstanley)
  • 27a8cf99a81d1bd5ab875cd773ac8647320b02ba: feat: implement uv Drawable for Canvas and Layer (@​aymanbagabas)
  • c4c08fc4f8a107b00bc54407ad9094b9642dd103: feat: implement uv.Drawable for *Layer (#​607) (@​ayn2op)
  • 18b4bb86c515f93eede5720fe66b0d9ba83fa489: feat: initial implementation of color blending & brightness helpers (@​lrstanley)
  • 63610090044b782caa8ce8b1b53cc81b98264eaa: feat: update examples/layout to use colors.BlendLinear1D (@​lrstanley)
  • de4521b8baa33c49a96e9458e9d9213c7ba407bd: feat: update examples/list/sublist to use colors.BlendLinear1D (@​lrstanley)
  • 1b3716cc53b5cc29c2b1b0c655a684b797fef075: feat: use custom hex parsing for increased perf (@​lrstanley)
Fixed
  • 06ca257e382fa107afcfe147c9cda836b3cdb4be: fix(canvas): Hit method should return Layer ID as string instead of *Layer (@​aymanbagabas)
  • d1fa8790efbd70df8b0dd8bd139434f3ac6e063b: fix(canvas): handle misc edge cases (#​588) (@​lrstanley)
  • 7869489d8971e2e3a8de8e0a4a1e1dfe4895a352: fix(canvas): simplify Render handling (@​aymanbagabas)
  • 68f38bdee72b769ff9c137a4097d9e64d401b703: fix(ci): use local golangci config (@​aymanbagabas)
  • ff11224963a33f6043dfb3408e67c7fea7

Configuration

📅 Schedule: Branch creation - "before 9am on monday" (UTC), Automerge - At any time (no schedule defined).

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about these updates again.


  • If you want to rebase/retry this PR, check this box

This PR was generated by Mend Renovate. View the repository job log.

@renovate
Copy link
Copy Markdown
Contributor Author

renovate bot commented Feb 16, 2026

ℹ️ Artifact update notice

File name: go.mod

In order to perform the update(s) described in the table above, Renovate ran the go get command, which resulted in the following additional change(s):

  • 1 additional dependency was updated

Details:

Package Change
github.com/charmbracelet/x/ansi v0.11.5 -> v0.11.6

@renovate renovate bot force-pushed the renovate/major-go-deps branch from 6119fea to a85cb5e Compare February 24, 2026 18:38
@renovate renovate bot changed the title fix(deps): update module github.com/charmbracelet/bubbles to v1 fix(deps): update go dependencies to v2 (major) Feb 24, 2026
@renovate
Copy link
Copy Markdown
Contributor Author

renovate bot commented Feb 24, 2026

⚠️ Artifact update problem

Renovate failed to update an artifact related to this branch. You probably do not want to merge this PR as-is.

♻ Renovate will retry this branch, including artifacts, only when one of the following happens:

  • any of the package files in this branch needs updating, or
  • the branch becomes conflicted, or
  • you click the rebase/retry checkbox if found above, or
  • you rename this PR's title to start with "rebase!" to trigger it manually

The artifact failure details are included below:

File name: go.sum
Command failed: go get -t ./...
go: github.com/charmbracelet/bubbles/v2@v2.1.0: parsing go.mod:
	module declares its path as: charm.land/bubbles/v2
	        but was required as: github.com/charmbracelet/bubbles/v2

@renovate renovate bot force-pushed the renovate/major-go-deps branch 2 times, most recently from b87d803 to cf648c6 Compare March 9, 2026 14:00
@renovate renovate bot force-pushed the renovate/major-go-deps branch from cf648c6 to 9d723c8 Compare March 11, 2026 17:54
@renovate renovate bot force-pushed the renovate/major-go-deps branch from 9d723c8 to 2c6663f Compare March 26, 2026 20:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants