An Introduction to Charmbracelet/Bubble Tea
Here's a breakdown of its benefits for software engineers, how to get started, and a simple code example.
From a software engineer's perspective, Bubble Tea is a game-changer for a few key reasons
Modern TUI Development
It moves beyond the limitations of simple, line-based CLIs, allowing you to build rich, interactive interfaces directly in the terminal. Think of things like dashboards, forms, and progress bars.
Inspired by The Elm Architecture
Bubble Tea is based on a predictable, state-driven architecture. This makes your TUI applications easy to reason about, test, and maintain. The core concepts are a Model (your application's state), Update (how you handle messages and change the state), and View (how you render the state to the terminal).
Go-native
As a Go framework, it fits perfectly into the existing Go ecosystem. You get the benefits of Go's performance, concurrency, and tooling. Plus, it's a single, statically linked binary, which makes distribution incredibly simple.
Cross-platform Compatibility
Your TUI application will work on various operating systems without any special changes, as long as it's a modern terminal.
<br>
<br>
Getting up and running with Bubble Tea is straightforward.
First, you need to install the Bubble Tea library in your Go project. You can do this with the standard go get command
go get github.com/charmbracelet/bubbletea
To build a Bubble Tea application, you'll work with three main components
model
A struct that holds the state of your application.
Init()
A function that returns an initial command, which is often nil for simple applications.
Update()
This function receives messages (like key presses) and returns a new model and an optional command. This is where your application's logic lives.
View()
This function takes your model and returns a string that represents what the user sees in the terminal.
You'll create a new program by initializing your model and then running it with tea.NewProgram. The program will handle the TUI lifecycle for you.
<br>
<br>
Let's build a simple Bubble Tea app that displays a message and quits when you press 'q'.
package main
import (
"fmt"
"os"
tea "github.com/charmbracelet/bubbletea"
)
// Define our application's state.
// This is the model.
type model struct {
quitting bool
}
// Init returns the initial command for the application.
func (m model) Init() tea.Cmd {
return nil
}
// Update handles messages and updates the model.
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "q", "esc", "ctrl+c":
m.quitting = true
return m, tea.Quit
}
}
return m, nil
}
// View returns the string representation of the UI.
func (m model) View() string {
if m.quitting {
return " See ya later!\n\n"
}
s := "Hello, Bubble Tea! Press 'q' to quit.\n"
return s
}
func main() {
p := tea.NewProgram(model{})
if _, err := p.Run(); err != nil {
fmt.Printf("Uh oh, there was an error: %v\n", err)
os.Exit(1)
}
}
We define a model struct to hold our application state. In this case, it's just a quitting boolean.
Init() returns nil because we don't have any initial commands to run.
Update() checks for keyboard messages. If the user presses 'q', 'esc', or 'ctrl+c', we set quitting to true and return tea.Quit, which tells the program to exit gracefully.
View() checks the quitting state. If true, it displays the goodbye message; otherwise, it shows the main "Hello" message.
In main(), we create a new program with our initial model and call p.Run().