# Bubble Tea User Guide

Bubble Tea is a Go framework for building rich terminal user interfaces (TUIs). It is based on **The Elm Architecture** — a simple, functional, and stateful pattern where your entire application is described by a single **model**, an **update** function that reacts to events, and a **view** that renders the current state. Bubble Tea takes care of input, rendering, and the redraw loop so you can focus on your application's logic.

This guide covers **Bubble Tea v2** (module path `charm.land/bubbletea/v2`). v2 is a significant evolution of the original `github.com/charmbracelet/bubbletea`; if you are migrating an existing app, jump to [Upgrading from v1](#upgrading-from-v1) and the dedicated `UPGRADE_GUIDE_V2.md` shipped in the repository.

> [!NOTE]
> Bubble Tea is a **library you import**, not a command-line tool you install and run. There are no `bubbletea` subcommands. You build your own program against the `tea` package, and *your* binary is what users run.

---

## 1. Quick Start

Here is a complete, runnable Bubble Tea program — a counter you increment with the spacebar.

```go
package main

import (
	"fmt"
	"os"

	tea "charm.land/bubbletea/v2"
)

// model holds your entire application state.
type model struct {
	count int
}

// Init runs once at startup and returns an optional initial command.
func (m model) Init() tea.Cmd {
	return nil
}

// Update reacts to messages (events) and returns the next model + an optional command.
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.KeyPressMsg:
		switch msg.String() {
		case "ctrl+c", "q":
			return m, tea.Quit
		case "space":
			m.count++
		}
	}
	return m, nil
}

// View renders the UI from the current model.
func (m model) View() tea.View {
	return tea.NewView(fmt.Sprintf("Count: %d\n\nPress space to increment, q to quit.\n", m.count))
}

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

Run it:

```bash
go run .
```

Expected output (interactive — updates in place as you press space):

```text
Count: 3

Press space to increment, q to quit.
```

> [!TIP]
> Every Bubble Tea program is just these four pieces: a `model` type, and `Init`, `Update`, and `View` methods on it. Once you understand this loop, you understand Bubble Tea.

---

## 2. Installation & Requirements

### Requirements

| Requirement | Detail |
|---|---|
| **Go** | 1.25 or newer (the module declares `go 1.25.0`) |
| **Terminal** | Any modern terminal emulator. Advanced features (true color, keyboard enhancements, synchronized output) are detected and gracefully degraded when unsupported. |
| **OS** | Linux, macOS, BSD, Windows, and more. Windows uses the Console VT API; most Unix-like systems are supported via termios. |

### Add it to your project

```bash
go get charm.land/bubbletea/v2
```

Then import it (the conventional alias is `tea`):

```go
import tea "charm.land/bubbletea/v2"
```

> [!IMPORTANT]
> The v2 import path is the vanity domain **`charm.land/bubbletea/v2`**, *not* `github.com/charmbracelet/bubbletea`. Using the old path pulls in v1, which has a different API.

Run `go mod tidy` to download Bubble Tea and its dependencies.

### Pair it with the ecosystem

Bubble Tea is the runtime. For ready-made widgets and styling you'll almost always pair it with its sibling libraries:

| Library | Purpose | Import |
|---|---|---|
| **Bubbles** | Common components: text input, list, table, viewport, spinner, paginator, help, file picker, text area, progress, stopwatch, timer | `charm.land/bubbles/v2/...` |
| **Lip Gloss** | Style, color, and layout (borders, padding, alignment, joins) | `charm.land/lipgloss/v2` |
| **Glamour** | Render Markdown in the terminal | `charm.land/glamour/v2` |
| **Harmonica** | Spring-physics animation | `github.com/charmbracelet/harmonica` |

> [!NOTE]
> Bubbles and Lip Gloss are separate modules with their own v2 import paths. This guide documents Bubble Tea itself; component APIs (like `textinput.Model`) belong to Bubbles.

---

## 3. Core Concepts: The Elm Architecture

Bubble Tea applications have exactly three moving parts, all defined by the `tea.Model` interface:

```go
type Model interface {
	Init() Cmd                  // startup: return an optional initial command
	Update(Msg) (Model, Cmd)    // event handling: react to a message
	View() View                 // rendering: describe the UI
}
```

```text
            ┌─────────────────────────────────────────┐
            │                                          │
   start ──►│  Init() ──► initial Cmd                  │
            │                                          │
            │     ┌──────────────┐                     │
   event ──►│ ──► │   Update()   │ ──► (Model, Cmd) ──┐ │
   (Msg)    │     └──────────────┘                   │ │
            │             ▲                           │ │
            │             │   Cmd runs ──► new Msg ───┘ │
            │             │                             │
            │     ┌──────────────┐                      │
            │     │    View()    │ ──► rendered to screen│
            │     └──────────────┘                      │
            └─────────────────────────────────────────┘
```

### Model — your state

The model is any Go type (usually a `struct`) that holds everything your app needs to know. It is the single source of truth.

```go
type model struct {
	choices  []string
	cursor   int
	selected map[int]struct{}
}
```

### Init — the startup hook

`Init` runs once before the first render and returns an initial `tea.Cmd` (or `nil` for "do nothing yet"). Use it to kick off work like an HTTP request, a timer, or a blinking cursor.

```go
func (m model) Init() tea.Cmd {
	return nil // no initial I/O
}
```

### Update — react to messages

Whenever *anything* happens — a keypress, a window resize, a command finishing — Bubble Tea calls `Update` with a `tea.Msg`. You inspect the message (usually with a type switch), return an updated model, and optionally return a command to do more work.

```go
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.KeyPressMsg:
		switch msg.String() {
		case "up", "k":
			if m.cursor > 0 {
				m.cursor--
			}
		case "down", "j":
			if m.cursor < len(m.choices)-1 {
				m.cursor++
			}
		case "enter", "space":
			if _, ok := m.selected[m.cursor]; ok {
				delete(m.selected, m.cursor)
			} else {
				m.selected[m.cursor] = struct{}{}
			}
		case "ctrl+c", "q":
			return m, tea.Quit
		}
	}
	return m, nil
}
```

> [!IMPORTANT]
> `Update` must **return the updated model**. Bubble Tea uses the returned model for the next render and the next update. If you mutate a copy and forget to return it, your changes are lost. (When your model is a value type, you modify the copy `m` and return it; when it's a pointer, you can return the same pointer.)

### View — describe the UI

`View` looks at the current model and returns a `tea.View` describing what to show. You never call it yourself and you never manage redraws — Bubble Tea calls `View` after every update and diffs the output for you.

```go
func (m model) View() tea.View {
	s := "What should we buy?\n\n"
	for i, choice := range m.choices {
		cursor := " "
		if m.cursor == i {
			cursor = ">"
		}
		checked := " "
		if _, ok := m.selected[i]; ok {
			checked = "x"
		}
		s += fmt.Sprintf("%s [%s] %s\n", cursor, checked, choice)
	}
	s += "\nPress q to quit.\n"
	return tea.NewView(s)
}
```

> [!TIP]
> In v2, `View()` returns a **`tea.View` struct**, not a `string`. `tea.NewView("...")` is the shortcut for "just this text." When you need terminal features like full-screen mode or mouse tracking, you set fields on the View — see [Section 5](#5-the-view-controlling-the-terminal).

---

## 4. Commands and Messages

**Messages** (`tea.Msg`) are events. **Commands** (`tea.Cmd`) are how you *cause* events — especially the results of I/O. This separation is what keeps Bubble Tea apps responsive: slow work runs off the main loop and reports back as a message.

A command is simply a function that does some work and returns a message:

```go
type Cmd func() Msg
```

### Writing a command

Here a command performs an HTTP request and reports the result back as a message:

```go
const url = "https://charm.sh/"

type statusMsg int
type errMsg struct{ err error }

func (e errMsg) Error() string { return e.err.Error() }

// checkServer is a tea.Cmd: a func() tea.Msg.
func checkServer() tea.Msg {
	c := &http.Client{Timeout: 10 * time.Second}
	res, err := c.Get(url)
	if err != nil {
		return errMsg{err}
	}
	defer res.Body.Close()
	return statusMsg(res.StatusCode)
}

func (m model) Init() tea.Cmd {
	return checkServer // run it at startup
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case statusMsg:
		m.status = int(msg)
		return m, tea.Quit
	case errMsg:
		m.err = msg
		return m, tea.Quit
	}
	return m, nil
}
```

> [!CAUTION]
> **Never do blocking I/O directly inside `Update`.** `Update` runs on the main loop; blocking it freezes your whole UI. Put slow work inside a command instead — Bubble Tea runs commands concurrently and feeds the resulting message back to `Update`.

### Built-in commands

These functions return commands (or special messages) you return from `Init`/`Update`:

| Command | Returns | What it does |
|---|---|---|
| `tea.Quit` | special | Tells the program to exit gracefully. |
| `tea.Interrupt` | special | Exits with `ErrInterrupted` (like Ctrl+C semantics). |
| `tea.Suspend` | special | Suspends the program (like Ctrl+Z); you get a `ResumeMsg` on return. |
| `tea.Batch(cmds...)` | `Cmd` | Run several commands **concurrently**, no ordering guarantees. |
| `tea.Sequence(cmds...)` | `Cmd` | Run several commands **one at a time, in order**. |
| `tea.Tick(d, fn)` | `Cmd` | Fire once after duration `d`, independent of the system clock. |
| `tea.Every(d, fn)` | `Cmd` | Fire once, synced to the system clock (e.g. on the next whole second). |
| `tea.Exec(c, fn)` / `tea.ExecProcess(c, fn)` | `Cmd` | Pause the program and run an external process (editor, shell). |
| `tea.Println(args...)` / `tea.Printf(fmt, args...)` | `Cmd` | Print a line *above* the program that persists across renders. |
| `tea.ClearScreen` | special | Clear the screen before the next render (inline mode). |
| `tea.SetClipboard(s)` / `tea.ReadClipboard` | `Cmd` / special | Write/read the system clipboard via OSC52. |
| `tea.SetPrimaryClipboard(s)` / `tea.ReadPrimaryClipboard` | `Cmd` / special | X11/Wayland primary selection. |
| `tea.RequestWindowSize` | special | Ask for the current terminal size (delivered as `WindowSizeMsg`). |
| `tea.RequestBackgroundColor` / `tea.RequestForegroundColor` / `tea.RequestCursorColor` | special | Query terminal colors. |
| `tea.RequestCursorPosition` | special | Query the cursor position (`CursorPositionMsg`). |
| `tea.RequestCapability(name)` | `Cmd` | Query a terminfo capability (e.g. `"RGB"`, `"Tc"`). |
| `tea.RequestTerminalVersion` | special | Query the terminal name/version (XTVERSION). |
| `tea.Raw(seq)` | `Cmd` | Send a raw escape sequence to the terminal (advanced). |

> [!NOTE]
> Some of these (`tea.Quit`, `tea.ClearScreen`, `tea.RequestWindowSize`, `tea.ReadClipboard`, …) are functions you pass *by name* (they are themselves `tea.Cmd` values). Others (`tea.Batch`, `tea.Tick`, `tea.SetClipboard`, …) you *call* to produce a command. Match the usage in the examples above.

### Looping a timer

`Tick` and `Every` fire only **once**. To keep ticking, return the command again from `Update`:

```go
type tickMsg time.Time

func tickEvery() tea.Cmd {
	return tea.Every(time.Second, func(t time.Time) tea.Msg {
		return tickMsg(t)
	})
}

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

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg.(type) {
	case tickMsg:
		// ... do per-second work ...
		return m, tickEvery() // schedule the next tick
	}
	return m, nil
}
```

### Built-in messages you'll receive

Bubble Tea translates terminal events into typed messages and delivers them to `Update`. Match the ones you care about:

| Message | When it arrives |
|---|---|
| `tea.KeyPressMsg` | A key was pressed. The most common message you'll handle. |
| `tea.KeyReleaseMsg` | A key was released (only with keyboard enhancements). |
| `tea.KeyMsg` | **Interface** covering both press and release — type-switch inside. |
| `tea.MouseClickMsg` / `tea.MouseReleaseMsg` / `tea.MouseWheelMsg` / `tea.MouseMotionMsg` | Mouse events (when a mouse mode is enabled). |
| `tea.MouseMsg` | **Interface** covering all mouse events — call `.Mouse()` for coordinates. |
| `tea.WindowSizeMsg` | Sent on startup and on every terminal resize. Has `Width`, `Height`. |
| `tea.FocusMsg` / `tea.BlurMsg` | Terminal gained/lost focus (when `ReportFocus` is on). |
| `tea.PasteMsg` / `tea.PasteStartMsg` / `tea.PasteEndMsg` | Bracketed-paste content and boundaries. |
| `tea.ColorProfileMsg` | The detected color profile (sent at startup). |
| `tea.BackgroundColorMsg` / `tea.ForegroundColorMsg` / `tea.CursorColorMsg` | Terminal color query results. Has `IsDark()`. |
| `tea.EnvMsg` | The program's environment (useful over SSH). |
| `tea.KeyboardEnhancementsMsg` | Which keyboard enhancements the terminal supports. |
| `tea.ResumeMsg` | The program resumed after a suspend. |
| `tea.WindowSizeMsg` is auto-delivered — you don't need to request it for normal use. | |

> [!TIP]
> A custom message is just any type. Define your own (`type myMsg struct{...}`) and send it from a command. That's the idiomatic way to get the result of background work into `Update`.

---

## 5. The View: Controlling the Terminal

In v2, terminal features are **declarative**: you set fields on the `tea.View` returned by `View()`, every render. There are no startup flags or toggle commands — you simply describe the state you want, and Bubble Tea makes the terminal match it.

```go
func (m model) View() tea.View {
	v := tea.NewView("Hello, full screen!")
	v.AltScreen = true                      // take over the whole window
	v.MouseMode = tea.MouseModeCellMotion    // track mouse clicks/drags
	v.WindowTitle = "My App"                 // set the terminal title
	return v
}
```

### View fields reference

| Field | Type | What it controls |
|---|---|---|
| `Content` | `string` | The rendered text. Set with `tea.NewView("…")` or `v.SetContent("…")`. |
| `AltScreen` | `bool` | Full-window "alternate screen" mode. Auto-restored on exit. |
| `MouseMode` | `MouseMode` | `MouseModeNone`, `MouseModeCellMotion`, or `MouseModeAllMotion`. |
| `ReportFocus` | `bool` | Deliver `FocusMsg`/`BlurMsg` when the terminal gains/loses focus. |
| `DisableBracketedPasteMode` | `bool` | Turn off bracketed paste for this view. |
| `WindowTitle` | `string` | The terminal window title. |
| `Cursor` | `*Cursor` | Show/position/style the cursor. `nil` hides it. |
| `ForegroundColor` | `color.Color` | Override the terminal foreground color (`nil` = default). |
| `BackgroundColor` | `color.Color` | Override the terminal background color (`nil` = default). |
| `ProgressBar` | `*ProgressBar` | Native terminal progress bar (where supported). |
| `KeyboardEnhancements` | `KeyboardEnhancements` | Request key repeat/release and other enhanced reporting. |
| `OnMouse` | `func(MouseMsg) Cmd` | Per-view mouse handler based on rendered content. |

### Full-screen vs. inline

- **Inline** (default): your UI renders in the normal terminal flow, scrolling with history. Great for prompts, spinners, and progress.
- **Alt screen** (`v.AltScreen = true`): your UI takes over the entire window like `vim` or `less`. On exit, the previous terminal contents are restored.

```go
// Toggle full screen with a key:
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.KeyPressMsg:
		if msg.String() == "f" {
			m.fullscreen = !m.fullscreen
		}
	}
	return m, nil
}

func (m model) View() tea.View {
	v := tea.NewView("Press f to toggle full screen.")
	v.AltScreen = m.fullscreen
	return v
}
```

### Showing and styling the cursor

```go
v := tea.NewView("Type here: ")
v.Cursor = tea.NewCursor(11, 0) // x, y; visible block cursor that blinks
v.Cursor.Shape = tea.CursorBar  // CursorBlock, CursorUnderline, or CursorBar
v.Cursor.Blink = false
```

Set `v.Cursor = nil` to hide the cursor entirely.

### Native terminal progress bar

```go
v := tea.NewView("Downloading…")
v.ProgressBar = tea.NewProgressBar(tea.ProgressBarDefault, 42) // 0–100
```

States: `ProgressBarNone`, `ProgressBarDefault`, `ProgressBarError`, `ProgressBarIndeterminate`, `ProgressBarWarning`. Support depends on the terminal (e.g. Windows Terminal).

> [!NOTE]
> The View progress bar is the terminal's *own* progress indicator (taskbar/tab). For an in-content progress bar widget, use the `progress` component from Bubbles.

---

## 6. Handling Keyboard Input

Match `tea.KeyPressMsg` and compare against `msg.String()` — the readable, human-friendly key name.

```go
case tea.KeyPressMsg:
	switch msg.String() {
	case "enter":
		// submit
	case "ctrl+c", "q":
		return m, tea.Quit
	case "up", "k":
		m.cursor--
	case "shift+tab":
		// previous field
	}
```

### Key naming you should know

| Key | `String()` returns |
|---|---|
| Spacebar | `"space"` (not `" "`) |
| Enter / Return | `"enter"` |
| Escape | `"esc"` |
| Arrows | `"up"`, `"down"`, `"left"`, `"right"` |
| With modifiers | `"ctrl+c"`, `"alt+enter"`, `"shift+tab"`, `"ctrl+shift+a"` |

> [!IMPORTANT]
> The spacebar's `String()` is `"space"`. (Its `Code` is still the rune `' '` and its `Text` is still `" "` — but match on `"space"` when using `String()`.)

### Field-level matching

For maximum precision, inspect the `Key` directly instead of its string:

```go
case tea.KeyPressMsg:
	key := msg.Key()
	switch key.Code {
	case tea.KeyEnter:
		// enter
	case tea.KeyEsc:
		// escape
	default:
		if key.Text != "" {
			// a printable character was typed: key.Text
		}
	}
```

The `Key` struct fields:

| Field | Meaning |
|---|---|
| `Code` | The key as a rune or special constant (`KeyEnter`, `KeyF1`, `'a'`, …). |
| `Text` | The printable character(s), if any (empty for special/modified keys). |
| `Mod` | Modifier bitmask. Test with `key.Mod.Contains(tea.ModCtrl)`, etc. |
| `ShiftedCode` | The shifted key (e.g. `'A'` for shift+a). Kitty protocol / Windows only. |
| `BaseCode` | The key on a US PC-101 layout (handy for international keyboards). |
| `IsRepeat` | Whether the key is auto-repeating. Kitty protocol / Windows only. |

Modifier constants: `tea.ModShift`, `tea.ModAlt`, `tea.ModCtrl`, `tea.ModMeta`, `tea.ModSuper`, `tea.ModHyper`, plus lock states `tea.ModCapsLock`, `tea.ModNumLock`, `tea.ModScrollLock`.

### Keyboard enhancements (key repeat & release)

Some terminals (via the Kitty keyboard protocol) can report key *releases* and richer key data. Request it via the View:

```go
func (m model) View() tea.View {
	v := tea.NewView("Press keys…")
	v.KeyboardEnhancements.ReportEventTypes = true // enable release/repeat events
	return v
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.KeyboardEnhancementsMsg:
		if msg.SupportsEventTypes() {
			// great — we'll get KeyReleaseMsg too
		}
	case tea.KeyReleaseMsg:
		// a key was released
	}
	return m, nil
}
```

---

## 7. Handling Mouse Input

Enable a mouse mode on the View, then match the specific mouse message types.

```go
func (m model) View() tea.View {
	v := tea.NewView("Click anywhere.")
	v.MouseMode = tea.MouseModeCellMotion // clicks, releases, wheel, drags
	return v
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.MouseClickMsg:
		mouse := msg.Mouse()
		if mouse.Button == tea.MouseLeft {
			// left click at mouse.X, mouse.Y
		}
	case tea.MouseWheelMsg:
		if msg.Mouse().Button == tea.MouseWheelUp {
			// scrolled up
		}
	case tea.MouseMotionMsg:
		// movement (drag in CellMotion, any move in AllMotion)
	}
	return m, nil
}
```

### Mouse modes

| Mode | Captures |
|---|---|
| `tea.MouseModeNone` | Nothing (default). |
| `tea.MouseModeCellMotion` | Clicks, releases, wheel, and **drag** motion (motion while a button is held). Best supported. |
| `tea.MouseModeAllMotion` | All of the above **plus** motion with no buttons pressed. |

### Mouse buttons

`tea.MouseLeft`, `tea.MouseMiddle`, `tea.MouseRight`, `tea.MouseWheelUp`, `tea.MouseWheelDown`, `tea.MouseWheelLeft`, `tea.MouseWheelRight`, `tea.MouseBackward`, `tea.MouseForward`, `tea.MouseNone`.

> [!NOTE]
> Coordinates are zero-based with `(0,0)` at the top-left. In v2, `MouseMsg` is an interface — always call `.Mouse()` to get the `Mouse` struct (`X`, `Y`, `Button`, `Mod`).

---

## 8. Common Tasks (Recipes)

### Run an external program (editor, pager, shell)

Pause Bubble Tea, hand the terminal to another process, and resume when it exits.

```go
type editorFinishedMsg struct{ err error }

func openEditor() tea.Cmd {
	editor := cmp.Or(os.Getenv("EDITOR"), "vim")
	c := exec.Command(editor)
	return tea.ExecProcess(c, func(err error) tea.Msg {
		return editorFinishedMsg{err}
	})
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.KeyPressMsg:
		if msg.String() == "e" {
			return m, openEditor()
		}
	case editorFinishedMsg:
		if msg.err != nil {
			m.err = msg.err
			return m, tea.Quit
		}
	}
	return m, nil
}
```

### Send messages into a running program from outside

A `*Program` can receive messages from other goroutines via `p.Send`. This is how you feed in events from servers, channels, or background workers.

```go
p := tea.NewProgram(initialModel())

go func() {
	for activity := range someChannel {
		p.Send(activityMsg(activity)) // delivered to Update
	}
}()

if _, err := p.Run(); err != nil { /* ... */ }
```

### Print persistent lines above the UI

Use `tea.Println`/`tea.Printf` (as commands) to emit log-like lines that scroll into history above your live UI. (No effect while the alt screen is active.)

```go
return m, tea.Printf("processed item %d", n)
```

### Add a spinner (Bubbles)

```go
import "charm.land/bubbles/v2/spinner"

type model struct{ spinner spinner.Model }

func initialModel() model {
	s := spinner.New()
	s.Spinner = spinner.Dot
	return model{spinner: s}
}

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

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.KeyPressMsg:
		if msg.String() == "q" {
			return m, tea.Quit
		}
	default:
		var cmd tea.Cmd
		m.spinner, cmd = m.spinner.Update(msg)
		return m, cmd
	}
	return m, nil
}

func (m model) View() tea.View {
	return tea.NewView(fmt.Sprintf("%s Loading…\n", m.spinner.View()))
}
```

### Add a text input (Bubbles)

```go
import "charm.land/bubbles/v2/textinput"

func initialModel() model {
	ti := textinput.New()
	ti.Placeholder = "Pikachu"
	ti.Focus()
	ti.SetWidth(20)
	return model{textInput: ti}
}

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

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.KeyPressMsg:
		switch msg.String() {
		case "enter", "ctrl+c", "esc":
			return m, tea.Quit
		}
	}
	var cmd tea.Cmd
	m.textInput, cmd = m.textInput.Update(msg)
	return m, cmd
}
```

> [!TIP]
> The pattern for *every* Bubbles component is the same: hold its `Model` in your model, forward messages to `component.Update(msg)` in your `Update`, and embed `component.View()` in your `View`. This composition is the heart of building larger Bubble Tea apps.

### Composing multiple components

Forward messages to each child and assemble their views. Use Lip Gloss to lay them out:

```go
import "charm.land/lipgloss/v2"

func (m model) View() tea.View {
	str := lipgloss.JoinVertical(lipgloss.Top,
		m.headerView(),
		m.textInput.View(),
		m.footerView(),
	)
	return tea.NewView(str)
}
```

---

## 9. Workflows

### Build & ship your TUI

Because your Bubble Tea app is an ordinary Go program, you build and distribute it like any Go binary:

```bash
go build -o myapp .
./myapp
```

- [ ] Write your `model`, `Init`, `Update`, `View`
- [ ] `go mod tidy`
- [ ] `go build`
- [ ] Test in your target terminals (and over SSH if relevant)
- [ ] Distribute the single binary (cross-compile with `GOOS`/`GOARCH` as needed)

### Run a TUI over SSH

When serving a TUI to remote users, pass the *client's* environment so color and terminal detection are correct (the server's `os.Getenv` would be wrong):

```go
// sess is a github.com/charmbracelet/ssh Session
pty, _, _ := sess.Pty()
environ := append(sess.Environ(), "TERM="+pty.Term)
p := tea.NewProgram(model, tea.WithEnvironment(environ),
	tea.WithInput(sess), tea.WithOutput(sess))
```

> [!TIP]
> For full SSH-app scaffolding (auth, sessions, a TUI bastion), see Charm's **Wish** library. Bubble Tea provides `WithEnvironment`, `WithInput`, and `WithOutput` to plug into it.

### Headless / daemon mode

Disable the renderer to use the Elm Architecture for a non-TUI program (e.g. when output is not a TTY):

```go
p := tea.NewProgram(model, tea.WithoutRenderer())
```

Output and logs then go straight to stdout without redraw logic — handy for "TUI when interactive, plain logs when piped."

---

## 10. Debugging

### You can't log to stdout

Your TUI owns stdout, so `fmt.Println` will corrupt the display. Log to a **file** instead:

```go
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()
}
```

Then watch it live in another terminal:

```bash
tail -f debug.log
```

> [!NOTE]
> The `DEBUG` environment variable here is *your* convention, shown for illustration — Bubble Tea does not require it. Bubble Tea itself reads `TEA_TRACE` and `TEA_DEBUG`; see the Admin Guide for those.

### Debugging with Delve

Because Bubble Tea takes over stdin/stdout, run Delve headless and connect to it:

```bash
# Terminal 1: start the debugger
dlv debug --headless --api-version=2 --listen=127.0.0.1:43000 .
```

```text
API server listening at: 127.0.0.1:43000
```

```bash
# Terminal 2: connect
dlv connect 127.0.0.1:43000
```

Always pass `--listen` (otherwise the port changes per run) and `--api-version=2`.

---

## 11. Troubleshooting

### Error: `bubbletea: InitialModel cannot be nil`

**Cause:** You passed `nil` (or an unset model) to `tea.NewProgram`.
**Solution:** Pass an initialized model value: `tea.NewProgram(initialModel())`.
**Prevention:** Construct your model with a helper like `initialModel()` and pass it directly.

### Error: `bubbletea: error opening TTY: …`

**Cause:** Bubble Tea needs a controlling terminal for input but stdin isn't a TTY (e.g. input is piped) and no TTY could be opened.
**Solution:** Run the program in a real terminal, or supply input explicitly with `tea.WithInput(...)`, or disable input with `tea.WithInput(nil)` if your program doesn't need it.
**Prevention:** Don't pipe into an interactive TUI; for non-interactive use, run with `tea.WithoutRenderer()`.

### Garbage characters printed when the program quits too early

**Cause:** The program exits before the terminal finishes processing queued sequences (related to known issue #1590).
**Solution:** Let `Run()` return normally (use `tea.Quit`) rather than calling `os.Exit` from inside a goroutine; allow the graceful shutdown to restore the terminal.
**Prevention:** Quit via the `tea.Quit` command from `Update`, not by killing the process mid-render.

### My program shows garbage after a panic

**Cause:** A panic left the terminal in raw mode.
**Solution:** Bubble Tea catches panics by default and restores the terminal, printing the stack trace. If you disabled this with `tea.WithoutCatchPanics()`, re-enable it, or run `reset` in your shell to recover.
**Prevention:** Keep panic catching enabled (the default).

### Alt screen on macOS Terminal.app adds blank lines instead of a true alternate screen

**Cause:** Terminal.app's limited alt-screen support (known issue #1455).
**Solution:** Use a more capable terminal (iTerm2, Ghostty, WezTerm, kitty, Alacritty) for full alt-screen behavior.
**Prevention:** Test alt-screen UIs across the terminals your users actually use.

### Window resize isn't detected on Windows

**Cause:** Windows has no `SIGWINCH` signal, so resize handling differs from Unix (regression tracked in #1601).
**Solution:** Update to the latest Bubble Tea v2; rely on the `WindowSizeMsg` you receive and avoid assuming continuous resize events on Windows.
**Prevention:** Handle `WindowSizeMsg` whenever it arrives and recompute layout from it; don't hard-code dimensions.

### Wide characters / emoji break table or border alignment

**Cause:** Grapheme width calculation differs across terminals; some emoji (e.g. flags) render at unexpected widths (issues #1646, #1614).
**Solution:** Use the latest version (it negotiates Unicode "grapheme width" mode where supported) and prefer Lip Gloss/Bubbles components that account for display width.
**Prevention:** Test layouts with multi-byte and emoji content; avoid manual column math on raw strings.

### Mouse events aren't firing

**Cause:** No mouse mode is enabled.
**Solution:** Set `v.MouseMode = tea.MouseModeCellMotion` (or `AllMotion`) in `View()`.
**Prevention:** Remember mouse tracking is opt-in and lives on the View in v2 — there is no `WithMouse…` option anymore.

### Key releases never arrive

**Cause:** Key-release reporting requires keyboard enhancements, which not all terminals support.
**Solution:** Request it (`v.KeyboardEnhancements.ReportEventTypes = true`) and check the `KeyboardEnhancementsMsg` to confirm support before relying on `KeyReleaseMsg`.
**Prevention:** Treat releases as a progressive enhancement, not a guarantee.

---

## 12. Frequently Asked Questions

**Q: Is Bubble Tea a CLI tool I install and run?**
No. It's a Go library. You import `charm.land/bubbletea/v2`, build your own program, and run *that*.

**Q: Why does `View()` return a `tea.View` instead of a `string`?**
That's the v2 design. The View struct carries both your content *and* the terminal features you want (full screen, mouse, cursor, title, colors), so everything about the UI is declared in one place each render.

**Q: How do I enter full-screen mode?**
Set `v.AltScreen = true` on the View you return. There's no `WithAltScreen()` option in v2.

**Q: How do I run something slow without freezing the UI?**
Wrap it in a `tea.Cmd` (a `func() tea.Msg`) and return it from `Update`. Bubble Tea runs commands concurrently and delivers the result back as a message.

**Q: What's the difference between `Batch` and `Sequence`?**
`Batch` runs commands concurrently with no ordering. `Sequence` runs them one at a time, in order.

**Q: How do I get the terminal size?**
Handle `tea.WindowSizeMsg` — it's delivered automatically on startup and on every resize. You can also ask explicitly with `tea.RequestWindowSize`.

**Q: How do I update the model from a goroutine?**
Don't mutate the model directly from another goroutine. Instead call `p.Send(msg)` and update state inside `Update`. This preserves the single-threaded, predictable update loop.

**Q: Where do widgets like lists and text inputs come from?**
From **Bubbles** (`charm.land/bubbles/v2`), a separate component library. Styling comes from **Lip Gloss** (`charm.land/lipgloss/v2`).

**Q: My spacebar case `" "` stopped matching after upgrading. Why?**
In v2, `KeyPressMsg.String()` returns `"space"` for the spacebar. Change `case " ":` to `case "space":`.

**Q: How do I quit from outside the program?**
Call `p.Quit()` (graceful) or `p.Kill()` (immediate, skips the final render and returns `ErrProgramKilled`).

**Q: Does Bubble Tea work over SSH?**
Yes. Pass the client's environment with `tea.WithEnvironment(...)` and wire input/output to the SSH session (often via Charm's Wish).

**Q: Can I use Bubble Tea without a TUI?**
Yes — `tea.WithoutRenderer()` runs the architecture without drawing, useful for daemon-like programs or a non-interactive fallback when output isn't a TTY.

**Q: I'm on v1. How much work is the upgrade?**
Mostly mechanical: change the import path, make `View()` return `tea.View`, switch `KeyMsg`→`KeyPressMsg`, move feature flags into the View, and rename a few constants. See below.

---

## 13. Upgrading from v1

If you're coming from `github.com/charmbracelet/bubbletea`, the headline changes are:

1. **Import path** → `charm.land/bubbletea/v2` (and `charm.land/lipgloss/v2`).
2. **`View() string` → `View() tea.View`** (return `tea.NewView("…")`).
3. **`tea.KeyMsg` (struct) → `tea.KeyPressMsg`** for presses; `tea.KeyMsg` is now an interface for both press and release.
4. **Terminal features moved into the View** — `WithAltScreen()`, `EnterAltScreen`, `WithMouseCellMotion()`, `SetWindowTitle`, `HideCursor`, etc. are gone. Set `v.AltScreen`, `v.MouseMode`, `v.WindowTitle`, `v.Cursor` instead.
5. **Mouse:** `MouseMsg` is now an interface (call `.Mouse()`); buttons renamed (`MouseButtonLeft` → `MouseLeft`); match specific types (`MouseClickMsg`, …).
6. **Spacebar** `String()` is `"space"`, not `" "`.
7. **Renames:** `Sequentially` → `Sequence`; `WindowSize()` → `RequestWindowSize`.

> [!IMPORTANT]
> The repository ships a complete, table-by-table **`UPGRADE_GUIDE_V2.md`** with before/after code and a flat old→new lookup designed for search-and-replace (and for pointing an LLM at). Use it as your migration checklist.

For the full architectural rationale, configuration, and internals of v2, see the **Admin Guide**.

---

_Generated June 2026 from charmbracelet/bubbletea at a23da80. AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it. Unofficial; not affiliated with or reviewed by the project._
