charmbracelet/bubbletea
Program runtime
Active contributors: aymanbagabas, meowgorithm, muesli
Purpose
The runtime is the heart of Bubble Tea. It owns terminal state, drives the Elm-style event loop, runs commands concurrently, recovers from panics, and shuts everything down cleanly on quit, signal, or context cancellation. Almost every line of this code lives in one file: tea.go.
Directory layout
tea.go ← Program, Run, eventLoop, signals, resize, commands, panic recovery
options.go ← ProgramOptions
commands.go ← Cmd helpers (Quit/Tick/Every/Batch/Sequence)
tty.go ← initInputReader, initInput, restoreInput, OpenTTY, checkResize
signals_unix.go ← suspendProcess (SIGTSTP) on Unix
signals_windows.go ← suspendProcess no-op on Windows
termios_*.go ← per-OS termios constantsKey abstractions
| Type / func | File | Description |
|---|---|---|
Program (struct) |
tea.go |
Runtime instance. Holds channels, renderer, profile, TTYs, contexts, options. |
NewProgram |
tea.go |
Constructor; applies all ProgramOptions. |
Program.Run |
tea.go |
Blocking entry point. Initializes the TTY and runs the event loop. |
Program.eventLoop |
tea.go |
Selects on ctx.Done, errs, and msgs. |
Program.handleSignals |
tea.go |
Goroutine that listens for SIGINT/SIGTERM and pushes InterruptMsg/QuitMsg. |
Program.handleResize |
tea.go |
Goroutine that fires WindowSizeMsg on terminal resize. |
Program.handleCommands |
tea.go |
Goroutine pool that executes Cmds and forwards their Msgs. |
Program.execBatchMsg / execSequenceMsg |
tea.go |
Concurrent vs. sequential dispatch of multiple commands. |
Program.recoverFromPanic / recoverFromGoPanic |
tea.go |
Panic interceptors that restore the TTY before printing a stack trace. |
Program.shutdown |
tea.go |
sync.Once-guarded teardown sequence. |
Program.ReleaseTerminal / RestoreTerminal |
tea.go, tty.go |
Public API for handing the TTY to a child process. |
channelHandlers |
tea.go |
Internal helper that waits for all spawned goroutines on shutdown. |
How it works
sequenceDiagram
participant U as User code
participant P as Program
participant L as eventLoop
participant R as Renderer
participant I as Input reader
U->>P: NewProgram(model, opts...)
U->>P: Run()
P->>P: initTerminal (raw mode)
P->>P: pick renderer + detect color profile
P->>I: initInputReader
P->>P: handleSignals / handleResize / handleCommands
P->>L: model = initialModel; loop
L-->>R: render(model.View())
I-->>L: KeyPressMsg, MouseClickMsg, …
L->>U: model.Update(msg)
U-->>L: model', cmd
L->>P: cmds <- cmd
P->>L: msg = cmd()
L-->>R: render(model'.View())
Note over L,R: Loop continues until QuitMsg / ctx.Done()
P->>P: shutdown → restoreTerminalState
P-->>U: returnModel, returnErrStartup details (in Run)
- Validate that the model is non-nil; otherwise return early.
- Initialize
handlers(a freshchannelHandlersper run) and thecmds chan Cmd. - Defer
close(p.finished)soWaitreturns andcancel()so the internal context unblocks. - Open input. If
disableInputis set,p.input = nil. Otherwise default to stdin or open the controlling TTY (OpenTTYintty.go) when stdin is not itself a terminal. - Install signal handler if
disableSignalHandleris false. - Defer panic recovery if
disableCatchPanicsis false. - Set up TTYs and raw mode (
initTerminal→initInput). - Detect initial window size with
term.GetSize, build aWindowSizeMsg. - Choose a renderer:
nilRendererifdisableRenderer, otherwisenewCursedRenderer. Apply optimizations (hardTabs,backspace,mapNl). - Detect the color profile (
colorprofile.Detect) and send aColorProfileMsg. - Send the initial
WindowSizeMsgand anEnvMsgsnapshot of the environment. - Init the input reader and render the initial frame from
model.View(). - Optionally query synchronized output (DEC mode 2026) and unicode core (mode 2027) when
shouldQuerySynchronizedOutputreturns true. - Run the event loop until shutdown.
shouldQuerySynchronizedOutput makes a careful set of decisions: it skips SSH sessions unless the terminal is known-good (Windows Terminal, ghostty, wezterm, alacritty, kitty, rio).
The event loop
eventLoop selects on three channels:
| Channel | Action |
|---|---|
<-p.ctx.Done() |
Return without an error (graceful cancel). |
<-p.errs |
Return with the error. |
<-p.msgs |
Translate, filter, switch on type, forward to model.Update, schedule the returned Cmd, render the new view. |
Special internal messages handled before delegating to model.Update:
| Internal Msg | Effect |
|---|---|
QuitMsg |
Return from the loop. |
InterruptMsg |
Return with ErrInterrupted. |
SuspendMsg |
Call suspend() (TTY release → SIGTSTP → restore + ResumeMsg). |
CapabilityMsg |
Upgrade the color profile to TrueColor if the terminal advertised RGB. |
ModeReportMsg |
Enable synchronized output / unicode core in the renderer when the terminal confirms. |
MouseMsg |
Forward to renderer.onMouse for any view-level handling, then to the model. |
| Clipboard / color / cursor / capability / version requests | Translated to ANSI escape writes via Program.execute. |
BatchMsg / sequenceMsg |
Dispatched to execBatchMsg / execSequenceMsg goroutines. |
WindowSizeMsg |
Forwarded to renderer.resize. |
printLineMessage |
Forwarded to renderer.insertAbove. |
clearScreenMsg |
Forwarded to renderer.clearScreen. |
ColorProfileMsg |
Forwarded to renderer.setColorProfile. |
The filter set by WithFilter runs on every message and can replace it or drop it (return nil).
Concurrency invariants
p.msgsis the only path messages take toUpdate. If something can't be aMsg, it isn't dispatched.- Goroutines registered with
p.handlers.add(ch)close their channel on exit;handlers.shutdownwaits for all of them. Program.Send(msg)is safe to call from any goroutine; itselects on<-ctx.Done()to avoid blocking after teardown.- Panic recovery never tries to send on
p.errsif a previous error is already buffered; the channel is buffered at length 1.
Suspend / resume
SuspendMsg triggers Program.suspend() (tty.go):
releaseTerminal(true)— flushes output, restores tty, cancels the input reader.suspendProcess()— Unix sendsSIGTSTPto its own group viasignals_unix.go. On Windows the function is a stub.- After
SIGCONT,RestoreTerminal()puts the TTY back into raw mode, restarts the renderer, and reinitializes input. - A
ResumeMsgis sent so the model can refresh anything time-sensitive.
Exec
tea.Exec (see Exec) uses the same releaseTerminal/RestoreTerminal dance so a child process can take over the TTY.
Integration points
- Renderer: the runtime calls
renderer.start,render,flush,close,resize,setColorProfile,clearScreen,insertAbove,setSyncdUpdates,setWidthMethod, andonMouse. - Input:
tty.goownscancelReader,inputScanner, andreadLoopDone. The runtime defers toreadLoopfor streaming events intop.msgs. - Commands:
cmds chan CmdcoupleseventLoop(writer) tohandleCommands(reader). Commands run on their own goroutines. - Signals:
handleSignalsis the only path SIGINT/SIGTERM take into the model.
Entry points for modification
- Adding a new internal message type that the runtime reacts to: extend the type switch in
eventLoopand add the producer in the relevant subsystem (clipboard, screen, etc.). - Hooking a new lifecycle event (e.g. before render / after update): the cleanest insertion point is around the
model, cmd = model.Update(msg)call ineventLoop. Resist the urge to add ad-hoc callbacks — prefer messages. - Tuning shutdown ordering:
Program.shutdownissync.Once-guarded; changes there must preserve "renderer stopped before TTY restored".
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.