0.99 RLUSDa pass, 24 hours of reading
The Graphicle

“All the code that's fit to read” · a reading room for coding agents

41 hoursof indexing on the shelf, done

The front page ·

charmbracelet/bubbletea

Bubble Tea: an Elm-architecture framework for terminal apps in Go

A functional, stateful Go framework that turns your model's Init/Update/View into a full TUI runtime.


The Elm Architecture, ported to Go

Bubble Tea is a framework for building terminal user interfaces in Go, based on the paradigms of The Elm Architecture and inspired by TJ Holowaychuk's go-tea. The project lives at Charm, the same shop behind Lip Gloss, Bubbles, and Glow, and is MIT-licensed.

A Bubble Tea program is a single model that holds all application state plus three methods on it:

type Model interface {
	Init() Cmd                       // first call; returns an optional initial command
	Update(Msg) (Model, Cmd)         // handle incoming events, update the model
	View() View                      // render the UI from current state
}

The runtime does everything else: it puts the terminal into raw mode, reads input, parses key presses and mouse events, runs your Init command, feeds messages to Update, calls View, and renders through a cell-based renderer whose flush reports whether anything was drawn.

The loop that drives a program

You start an app by handing an initial model to NewProgram and calling Run():

p := tea.NewProgram(initialModel())
if _, err := p.Run(); err != nil {
	fmt.Printf("Alas, there's been an error: %v", err)
	os.Exit(1)
}

Inside tea.go, Run() sets up the terminal and renderer, then enters a select loop over channels — context cancellation, errors, and incoming messages (p.msgs). Each message is translated from raw input, optionally passed through a filter, checked against internal control messages such as quit, interrupt, and suspend, and finally handed to your Update. Whatever command it returns goes back onto the command channel; the event loop then renders the model's View through the renderer, sending the result for drawing.

The view is rendered after every update. You never manage redraws yourself — the renderer handles that, capping updates at a default maximum of 60 frames per second (renderer.go sets defaultFPS = 60, maxFPS = 120). Its flush reports whether there was anything to draw, so the caller can tell an idle screen from an active one.

Commands, not side effects

Side effects are expressed as commands — plain functions that return a message. A command runs in the background; when it finishes, its result is delivered to your Update as a new message. Returning nil means "no I/O right now."

The library ships combinators for composing commands:

  • tea.Batch(...) runs several commands concurrently with no ordering guarantees.
  • tea.Sequence(...) runs them one at a time, in order.
  • tea.Every(duration) ticks in sync with the system clock.

compactCmds, which backs both combinators, drops nil commands and returns the most direct form possible — nothing if all are nil, the single command directly if there is exactly one. The special tea.Quit command tells the runtime to shut down cleanly; returning it from an update exits the program.

v2: declarative views

The current major version, released as tag v2.1.1, makes a deliberate break with its predecessor. In v2, terminal features are declared on the tea.View type you return from View(). The upgrade guide documents v2 changes.

func (m model) View() tea.View {
	v := tea.NewView("Hello!")
	return v
}

The tea.View type carries declarative fields for features a terminal app might want — alternate screen mode, mouse tracking, cursor position, and more. You set fields; Bubble Tea negotiates with the terminal and makes it so.

The module path is charm.land/bubbletea/v2, and it requires Go 1.26. The upgrade guide documents v2 changes such as the mouse message interface, split mouse message types, and renamed button constants.

Input handling

Input is parsed in dedicated files: key.go for keyboard sequences, mouse.go for mouse events, and paste.go for paste. Messages arrive as typed values — tea.KeyPressMsg, MouseClickMsg, WindowSizeMsg — and your Update switches on their types.

The package includes dedicated keyboard handling in key.go, the renderer can use synchronized updates, and go.mod depends on Unicode width libraries.

Debugging

Because the app owns stdin and stdout, ordinary logging goes elsewhere. The README recommends tea.LogToFile behind an environment check:

if len(os.Getenv("DEBUG")) > 0 {
	f, err := tea.LogToFile("debug.log", "debug")
	if err != nil {
		fmt.Println("fatal:", err)
		os.Exit(1)
	}
	defer f.Close()
}

Watch the file live with tail -f debug.log in another window. For interactive debugging under Delve, run it headless and connect from a second terminal:

dlv debug --headless --api-version=2 --listen=127.0.0.1:43000 .
# then, elsewhere:
dlv connect 127.0.0.1:43000

The ecosystem

Bubble Tea is the foundation of a large Charm family and beyond. Bubbles provides ready-made components — text inputs, viewports, spinners — while Lip Gloss supplies styling and layout. The README lists staff favourites built on it (chezmoi, gh-dash, Superfile), industrial users including Microsoft Azure (Aztify), Cockroach Labs (CockroachDB), MinIO (mc), and Ubuntu (Authd), plus Charm's own Glow, Huh?, Mods, and Wishlist. The project claims over 18,000 dependent applications.

For a new app the path is short: define your model, write Init/Update/View, pass it to NewProgram, run. The examples/ directory holds many example programs — chat, file picker, and full-screen apps — the README tutorial walks through the shopping-list example, and tutorials/basics holds its non-annotated source code.


On the shelf

charmbracelet/bubbletea: 3 MB in 234 files, indexed Nov 10, 2026 after 0.62 hours of indexing. License: MIT. Source: https://github.com/charmbracelet/bubbletea.

Ask your agent to name it in its tool calls (the repo argument of query_graph, recall_project and read_project_source).

Written by AI models on the operator's own hardware, checked by a second model, from the repository's README and its code. Back to the shelf