Releases: charmbracelet/bubbletea
v2.0.0-beta.6
Renderer Fixes
This new beta release includes a number of fixes related to the renderer.
Changelog
Fixed
- 4accb1c: fix: renderer: avoid requesting keyboard enhancements on close (@aymanbagabas)
- 6781fb5: fix: renderer: ensure we reset cursor style and color on close (@aymanbagabas)
- fa36fb1: fix: renderer: only move cursor to bottom when we're in the main screen (@aymanbagabas)
- d7c95b8: fix: renderer: sync buffer resize with frame rate (@aymanbagabas)
Thoughts? Questions? We love hearing from you. Feel free to reach out on X, Discord, Slack, The Fediverse, Bluesky.
v2.0.0-beta.5
Bubble Tea beta 5 is here!
We're excited to share the new Bubble Tea v2 beta 5 release with you! In this release, we focused on refining the View API, making it more declarative with a single source of truth for view-related properties.
Summary
All the changes since Beta 4 are to supporting a more declarative View type. This might sound like a lot but it's fairly simple in practice. This is what it looks like now:
func (m model) View() tea.View {
v := tea.NewView("Hi, mom!")
v.AltScreen = true
v.WindowTitle = "It’s all about Mom today"
}Why the change?
The general impetus for doing this is to eliminate the possibility of race conditions and greatly improve view performance. For example, the altscreen was previously triggered via a Cmd, which would not necessarily hit at the same moment the view was being rendered.
What else happened?
We've removed many view-based startup options and commands in favor of a more declarative approach using the View type. For example, WithMouseCellMotion() is now a property on View, i.e. view.MouseMode = tea.MouseModeCellMotion.
Now, the View() method returns a View struct that encapsulates all view-related properties. This change simplifies the API and makes it more intuitive to use with a single point of configuration for views.
The View API
Previously, Bubble Tea used functional options and commands to configure various view-related properties. This approach was flexible but led to a fragmented API surface. Should I use WithAltScreen() or EnterAltScreen? Is there a difference between them? Why can't I enable focus reporting with WithReportFocus() but not via a command?
These questions and inconsistencies led us to rethink how we handle view configuration. The new View struct consolidates all view-related properties, making it easier to understand and use with a single source of truth. I.e., this is how your view looks like, and these are its properties.
type View struct {
Layer Layer // Layer represents the content of the view
Cursor *Cursor // Position, style, color (nil = hidden)
BackgroundColor color.Color // Terminal default background color
ForegroundColor color.Color // Terminal default foreground color
WindowTitle string // Window title
ProgressBar *ProgressBar // Progress bar (nil = no progressbar)
AltScreen bool // Alternate screen buffer (fullscreen mode)
MouseMode MouseMode // Mouse event mode
ReportFocus bool // Focus/blur events
DisableBracketedPasteMode bool // Bracketed paste
DisableKeyEnhancements bool // Keyboard enhancements
KeyReleases bool // Key release events
UniformKeyLayout bool // Uniform key layout
}Options and Commands
We've removed many of the view-related options and commands that were previously set using functional options or commands. Instead, these properties are now part of the View struct returned by the View() method.
Removed options include:
WithAltScreen()→View.AltScreen = trueWithMouseCellMotion()→View.MouseMode = tea.MouseModeCellMotionWithMouseAllMotion()→View.MouseMode = tea.MouseModeAllMotionWithReportFocus()→View.ReportFocus = trueWithKeyReleases()→View.KeyReleases = trueWithUniformKeyLayout()→View.UniformKeyLayout = trueWithoutBracketedPaste()→View.DisableBracketedPasteMode = trueWithInputTTY()→ useOpenTTY()and set input/output manually
Removed commands include:
EnterAltScreen/ExitAltScreen→View.AltScreenSetBackgroundColor()→View.BackgroundColorSetForegroundColor()→View.ForegroundColorSetCursorColor()→View.Cursor.ColorSetWindowTitle()→View.WindowTitleEnableMouseCellMotion/EnableMouseAllMotion/DisableMouse→View.MouseMode
Model Interface
The Model interface has been updated to make View() return a View struct instead of a string. We know that a string is often more convenient and easier to work with, however, due to the number of view-related options we wanted to support, we felt it was best to encapsulate them in a dedicated struct.
You can still use strings for your views of course! You just need to wrap them in a NewView() call, or use view.SetContent(yourString).
// Before
type Model interface {
Init() Cmd
Update(Msg) (Model, Cmd)
View() string
// Or...
View() (string, *Cursor)
}
// After
type Model interface {
Init() Cmd
Update(Msg) (Model, Cmd)
View() View
}Example?
Let's get to the fun part! Here's a simple example that displays a text in the center of the screen using the alt-screen buffer.
package main
import (
"fmt"
"os"
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
)
type model struct {
width, height 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.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height
case tea.KeyPressMsg:
return m, tea.Quit
}
return m, nil
}
func (m model) View() tea.View {
var v tea.View
content := lipgloss.NewStyle().
Width(m.width).
Height(m.height).
AlignHorizontal(lipgloss.Center).
AlignVertical(lipgloss.Center).
Foreground(lipgloss.Cyan).
Render(" Bubble Tea Beta 5! ")
v.AltScreen = true
v.SetContent(content)
return v
}
func main() {
p := tea.NewProgram(model{})
if _, err := p.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Alas, there's been an error: %v", err)
}
}Like details?
Here’s the full changelog since v2.0.0-beta.4
Changelog
New!
- 7f2ee3d: feat(examples): add a canvas example (@aymanbagabas)
- 9ea9a8c: feat(examples): add clickable example (@aymanbagabas)
- e67ab35: feat(examples): add layer hit message to canvas example (@aymanbagabas)
- 19dc7c2: feat: add NewView helper function (@aymanbagabas)
- 94c70a4: feat: add OpenTTY to open the terminal's TTY (@aymanbagabas)
- f51fc36: feat: add helper method SetContent to View (@aymanbagabas)
- 9b921df: feat: add option to disable key enhancements (@aymanbagabas)
- 69b915a: feat: add support for setting terminal window title using the View API (@aymanbagabas)
- 4cafc09: feat: add support for terminal progress bars (#1499) (@aymanbagabas)
- f1eadf4: feat: embed mouse events in hit messages (@aymanbagabas)
- 1e6aaa5: feat: implement hit detection for layers in cursed renderer (@aymanbagabas)
- 98f59a4: feat: implement viewable model and layers (@aymanbagabas)
- c3a99ef: feat: input: use new uv input reader api (@aymanbagabas)
- 314b50c: feat: properly call nested sequenceMsg and batchMsg (@wolfmagnate)
- bd45f5b: feat: renderer: use the new promoted tv renderer (@aymanbagabas)
- 718c8e7: feat: restore WithoutRenderer option (@aymanbagabas)
- a73e084: feat: support setting terminal fg/bg using the View API (@aymanbagabas)
- 31fb503: feat: update to use uv package (@aymanbagabas)
Fixed
- 256e348: fix(examples): add newline to keyboard-enhancements example output (@aymanbagabas)
- 51804bc: fix(examples): ensure we have a newline at the end of the simple example (@aymanbagabas)
- 70e94a2: fix(keyboard): use kitty keyboard state command (@aymanbagabas)
- 3b687ff: fix(progress): reset on shutdown (#1511) (@caarlos0)
- f9df81b: fix(renderer): clear screen on exit when not using alt screen (@aymanbagabas)
- d4d69f6: fix(renderer): drop lines from top of buffer when frame height exceeds screen height (@aymanbagabas)
- 73b42b8: fix(renderer): only send non-empty layer hit messages (@aymanbagabas)
- 28ab4f4: fix(renderer): properly reset cursor position to start of line (#1472) (@aymanbagabas)
- 56ff714: fix(renderer): reset cursor style on close (@aymanbagabas)
- cf6866f: fix: add View to Model interface and introduce DisableRenderer field (@aymanbagabas)
- c76509a: fix: compact sequences like batches (#958) (@jdhenke)
- ab0bc8e: fix: correct event type name (@aymanbagabas)
- d2ecd81: fix: cursed_renderer: use Erase instead of Clear (@aymanbagabas)
- 875f553: fix: disable modifyOtherKeys for tmux (@aymanbagabas)
- c4303e1: fix: dry passed width and height to newCursedRenderer and send resize (@aymanbagabas)
- 30a273a...
v1.3.10
Changelog
Bug fixes
- 9edf69c: fix: handle setWindowTitleMsg and windowSizeMsg in eventLoop (@aymanbagabas)
Thoughts? Questions? We love hearing from you. Feel free to reach out on X, Discord, Slack, The Fediverse, Bluesky.
v1.3.9
Changelog
New Features
- 314b50c: feat: properly call nested sequenceMsg and batchMsg (@wolfmagnate)
Bug fixes
- 9e0e8f0: fix: recover from nested panics in Sequence and Batch commands (@aymanbagabas)
Other work
- 6e1282a: add example for the nested Sequence and Batch (@wolfmagnate)
- 0290af4: simplify case for BatchMsg (@wolfmagnate)
Thoughts? Questions? We love hearing from you. Feel free to reach out on Twitter, Discord, Slack, The Fediverse.
v1.3.8
Changelog
Bug fixes
- 21eecd5: fix: send batch commands to cmds channel instead of executing them in event loop (#1473) (@aymanbagabas)
Thoughts? Questions? We love hearing from you. Feel free to reach out on Twitter, Discord, Slack, The Fediverse.
v1.3.7
Changelog
Bug fixes
- 28ab4f4: fix(renderer): properly reset cursor position to start of line (#1472) (@aymanbagabas)
- c76509a: fix: compact sequences like batches (#958) (@jdhenke)
- f5da8d0: fix: handle nested SequenceMsg in event loop and use sync.WaitGroup f… (#1463) (@aymanbagabas)
- 80ea844: fix: lint issues in key_windows.go and tty_windows.go (@aymanbagabas)
Documentation updates
- c3136ed: docs(license): update copyright date range (@meowgorithm)
- 919805f: docs(readme): update footer art (@meowgorithm)
- f01583b: docs: show the correct branch in the build badge (@aymanbagabas)
Other work
- a81d6f3: ci: sync dependabot config (#1480) (@charmcli)
- 53609c1: ci: sync golangci-lint config (#1466) (@github-actions[bot])
Thoughts? Questions? We love hearing from you. Feel free to reach out on Twitter, Discord, Slack, The Fediverse.
v2.0.0-beta.4
How’s it going?
This is a small beta release with a few changes and a bunch of bug fixes. Enjoy!
Keyboard
WithKeyboardEnhancements has been split into three separate functions for clarity:
WithKeyReleases: Enables support for reporting key release events.WithUniformKeyLayout: Enables support for reporting key events in a uniform layout format.RequestKeyDisambiguation: Enables support for disambiguated key events.
Debugging Panics
You can now set TEA_DEBUG to true to dump panic logs to the current directory.
export TEA_DEBUG=1Changelog
New Features
- 3ce0c24: feat: create a panic trace file (#1446) (@aymanbagabas)
- 99a80e6: feat: keyboard: split keyboard enhancements into separate functions (@aymanbagabas)
Bug fixes
- 576a623: fix(example): fix
keyboard-enhancementsexample (@andreynering) - b62fc32: fix(examples): tui-daemon-combo: modelView must also implement Init and Update (@aymanbagabas)
- f97ba58: fix(key): add Keystroke() method to KeyPressMsg and KeyReleaseMsg (@aymanbagabas)
- b152063: fix(tutorials): update basics tutorial per v2 tea.Model changes (@meowgorithm)
- 0a63003: fix: Windows: first character input lost on successive programs (#1368) (@joshallenit)
- 78ed49b: fix: maintain exec output (#1276) (@raphaelvigee)
- 536b713: fix: remove unused keyboardc channel (@aymanbagabas)
- 65af983: fix: renderer: reset cursor state on close (@aymanbagabas)
- 185e281: fix: renderer: use mapnl when terminal is not a tty (@aymanbagabas)
Documentation updates
- af1fcd3: docs(examples): update textinput and textarea examples per Bubbles v2 API changes (#1425) (@meowgorithm)
- 1b0f489: docs(readme): cleanup badges (@meowgorithm)
- f502dc5: docs: add isbn form validation example (#1401) (@DerMaddis)
- a806274: docs: tidy (@bashbunni)
Other work
- ca9473b: ci: sync golangci-lint config (#1431) (@github-actions[bot])
- 42c169a: docs(examples/textinput): support virtual cursor (@meowgorithm)
Thoughts? Questions? We love hearing from you. Feel free to reach out on Twitter, The Fediverse, or on Discord.
v1.3.6
This version fixes some important issues regarding tea.Exec output after resuming from the executed program, and Windows losing input character on successive program execution.
Big thanks to @raphaelvigee and @joshallenit for working on these issues, you're the best!
Changelog
Bug fixes
- 0a63003: fix: Windows: first character input lost on successive programs (#1368) (@joshallenit)
- 78ed49b: fix: maintain exec output (#1276) (@raphaelvigee)
Documentation updates
- 1b0f489: docs(readme): cleanup badges (@meowgorithm)
Other work
Thoughts? Questions? We love hearing from you. Feel free to reach out on Twitter, The Fediverse, or on Discord.
v2.0.0-beta.3
Another one! Bubble Tea Beta 3!
This is a fairly small update that makes key enhancements support even better. Let's get into it.
go get github.com/charmbracelet/bubbletea/v2@v2.0.0-beta.3Keys
Better Keys by Default
Key disambiguation is now enabled by default, when supported. This means that ctrl+i, which would normally send a tab will now send an actual ctrl+i in supporting terminals. To check for support listen for tea.KeyboardEnhancementsMsg in your update:
switch msg := msg.(type) {
case tea.KeyboardEnhancementsMsg:
if msg.SupportsKeyDisambiguation() {
// Yay!
}
}Note that on Windows key disambiguation is always supported.
Does the terminal support key disambigation?
While we suggest designing your application in a way that gracefully upgrades when key disambiguation is available, you could detect for lack of support with a simple timeout:
// queryTimeoutMsg is a message that will get sent after a certain timeout.
type queryTimeoutMsg struct{}
// queryTimeout is a Bubble Tea command that waits for a timeout before sending a [queryTimeoutMsg].
func queryTimeout(d time.Duration) tea.Msg {
time.Sleep(200*time.Millisecond) // 200 ms is purly arbitrary
return queryTimeoutMsg{}
}
func (m model) Init() tea.Cmd {
return tea.Batch(
tea.RequestKeyboardEnhancements(),
queryTimout,
)
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyboardEnhancementsMsg:
// We have more keys, heh
m.hasEnhancements = true
case queryTimeoutMsg:
if !m.hasEnhancements {
// Terminal doesn't support keyboard enhancements :'(
m.quit = true
}
}
}Changelog
Bug fixes
- 0ed7727: fix(examples): we don't need to wait for background color query (#1407) (@aymanbagabas)
- 825e109: fix: always enable basic keyboard enhancements to disambiguate keys (@aymanbagabas)
- 07fa6f6: fix: don't send keyboard enhancements message on startup (@aymanbagabas)
- cb090c0: fix: lint: use isWindows() helper function (@aymanbagabas)
- 7aa6fec: fix: only request default key enhancements on non-windows platforms (@aymanbagabas)
Thoughts? Questions? We love hearing from you. Feel free to reach out on Twitter, The Fediverse, or on Discord.
v2.0.0-beta.2
Extra! Extra! Bubble Tea Beta 2!
We're honing in on v2! This update is fairly minor and makes some adjustments based on the awesome community feedback we've received. Let’s jump into it.
go get github.com/charmbracelet/bubbletea/v2@v2.0.0-beta.2Keys
Easier Mappings
Thanks to some feedback from our beta users (thank you! ❤️), key events now always return the textual value when using key.String(). You can use key.Keystroke() to get the keystroke string representation of the event. For example, pressing shift+h would give you k.String() == "H" and k.Keystroke() == "shift+h" when keyboard enhancements is on. We believe this will make input handling closer to the good parts of v1 and avoid some gotchas with international keyboards.
// Imagine the user pressed shift+h and key is a KeyMsg
key.String () == "H"
key.Keystroke() == "shift+h"
// Now imagine the user entered a "?"
key.String() == "?"
key.Keystroke() == "shift+/ on an American keyboard, shift+, on a French keyboard"Other Stuff
- We reverted the new trailing newline character on program exit. We found that while it's a nice touch that simplifies many programs, it can cause issues for others. This brings rendering in-line with v1, so there will be no action needed migrating from v1
- Fix concurrency and add deadlock prevention (courtesy @desertwitch)
- Fixed some examples
Thanks again to @desertwitch for his amazing work on concurrency and deadlock prevention 🤘
Changelog
New Features
- 60b6f30: feat: add keystroke representation to Key struct (#1399) (@aymanbagabas)
Bug fixes
- 929ff5a: Revert "fix: ensure terminal prompt is on a new line after program shutdown" (@aymanbagabas)
- f4c8567: fix(examples): credit-card-form: use VirtualCursor in inputs (@aymanbagabas)
- 551bf9c: fix(examples): textarea: remove debug log (@aymanbagabas)
- 62ca063: fix(key): windows: recognize function keys on Windows (#1405) (@aymanbagabas)
- 0f9cd5b: fix(panics): fix race condition and false nil error return (@desertwitch)
- a79ab3f: fix(tests): account for multiple p.Wait() (@desertwitch)
- ed4fde7: fix: lint issues (@caarlos0)
- 1b5fc27: fix: prevent deadlock on ctx cancel during msg processing (@desertwitch)
- aa7a240: fix: release p.Wait() on p.Kill() to prevent external deadlocks (@desertwitch)
- 1923181: fix: report only external ctx cancel as ctx error (@desertwitch)
- c56f776: fix: resolve race conditions caused by p.Kill() (@desertwitch)
- d67e48a: fix: windows: remove broken key_windows.go (@aymanbagabas)
Documentation updates
- 5eee00a: docs(examples): add keyboard enhancements demo (@meowgorithm)
- 35334b9: docs(examples): remove extraneous lines in query-term example (@meowgorithm)
- 3797acb: docs(readme): fix header image mis-caching (@meowgorithm)
- 4e7ab6c: docs(viewport): update for viewport left gutter API changes (@meowgorithm)
Other work
- 7858a14: ci: fix lint issues (@andreynering)
- 1a0062b: ci: fix lint issues (@andreynering)
- 0b18d1f: ci: sync dependabot config (#1403) (@charmcli)
- a46d16d: ci: sync golangci-lint config (#1379) (@github-actions[bot])
- 5a360c9: ci: sync golangci-lint config (#1394) (@github-actions[bot])
- bf33130: refactor(examples): use glamour/v2 (@aymanbagabas)
Thoughts? Questions? We love hearing from you. Feel free to reach out on Twitter, The Fediverse, or on Discord.

