Open-Source Wikis

/

Bubble Tea

/

Systems

/

Commands

charmbracelet/bubbletea

Commands

Active contributors: meowgorithm, aymanbagabas, muesli

Purpose

A Cmd is the main mechanism for I/O in a Bubble Tea program. The Update function returns (Model, Cmd); the runtime executes the command on a goroutine and feeds the resulting Msg back into the loop. This subsystem covers the Cmd type, the helpers built on top of it, and the dispatch logic that runs them.

Directory layout

commands.go      ← Cmd helpers (Batch, Sequence, Tick, Every, RequestWindowSize)
tea.go           ← internal helpers (execBatchMsg, execSequenceMsg, handleCommands)
                   plus the special sentinel commands Quit / Suspend / Interrupt
clipboard.go     ← SetClipboard / ReadClipboard / SetPrimaryClipboard / ReadPrimaryClipboard
color.go         ← RequestForegroundColor / RequestBackgroundColor / RequestCursorColor
cursor.go        ← RequestCursorPosition
screen.go        ← ClearScreen
termcap.go       ← RequestCapability
xterm.go         ← RequestTerminalVersion
raw.go           ← Raw (advanced escape sequence injection)
exec.go          ← Exec / ExecProcess

Key abstractions

Type / func File Description
Cmd tea.go func() Msg. The unit of asynchronous work.
Batch(cmds...) commands.go Returns a Cmd that produces a BatchMsg carrying the children. Concurrent execution.
Sequence(cmds...) commands.go Returns a Cmd that produces a sequenceMsg. Sequential execution.
compactCmds[T] commands.go Drops nil children and collapses to nil / single / T(slice).
Tick(d, fn) commands.go Fires once after d independent of system clock.
Every(d, fn) commands.go Fires once aligned with the system clock.
RequestWindowSize commands.go Asks the runtime to deliver a fresh WindowSizeMsg.
Quit, Suspend, Interrupt tea.go Special commands that emit the corresponding internal Msg.
Println, Printf renderer.go Print one line above the program's render area.
BatchMsg (public) / sequenceMsg (private) commands.go The internal types used to dispatch grouped commands.

How it works

graph TD
    M["Model.Update"] -->|returns Cmd| Loop["eventLoop"]
    Loop -->|cmds <- cmd| Pool["handleCommands goroutine pool"]
    Pool -->|go cmd()| Worker["new goroutine per Cmd"]
    Worker -->|Msg| Send["Program.Send"]
    Send --> Loop
    Loop -->|BatchMsg| Batch["execBatchMsg<br/>(parallel)"]
    Loop -->|sequenceMsg| Seq["execSequenceMsg<br/>(serial)"]
    Batch --> Send
    Seq --> Send

Why goroutines?

handleCommands reads from cmds chan Cmd. Each command spawns a fresh goroutine because some commands (Tick, Every, HTTP requests) block for a long time. The runtime cannot wait for them — that would freeze the loop and turn Update synchronous. Instead, the goroutine runs the command and posts its Msg back via Program.Send. There is no Cmd.Cancel; if you need cancellation, build it into the command (e.g. close a done channel that the command reads).

compactCmds

Both Batch and Sequence go through compactCmds:

  • nil children are dropped,
  • a single non-nil child is returned directly (skipping the wrapper Msg overhead),
  • otherwise the children are wrapped in a BatchMsg or sequenceMsg.

This is the reason patterns like tea.Batch(maybeNilCmd, anotherCmd) work — nils are cleaned up automatically.

Tick vs Every

// Independent of clock — fires d after Tick is invoked.
cmd := tea.Tick(time.Second, func(t time.Time) tea.Msg { return TickMsg(t) })

// Aligned with system clock — fires at the next d boundary.
cmd := tea.Every(time.Second, func(t time.Time) tea.Msg { return TickMsg(t) })

Use Every when you want multiple TUI elements to tick in sync. Use Tick when you want a precise interval since "now". Both emit a single message; rebroadcast by returning the command again from Update.

Special sentinel commands

Quit, Suspend, and Interrupt are exported Cmds that simply return the corresponding internal Msg (QuitMsg, SuspendMsg, InterruptMsg). The event loop intercepts these in its top-level switch before they reach the user's Update. There is no need to handle them inside your model — Quit always wins.

Println / Printf

tea.Println(args...) and tea.Printf(template, args...) emit a printLineMessage that the runtime forwards to renderer.insertAbove. They print above the rendered region in inline mode and are no-ops in altscreen mode (the alt buffer has no scrollback to insert into).

Terminal-query commands

A family of commands sends ANSI requests and is answered by terminal events:

Cmd Resulting Msg Underlying ANSI
RequestForegroundColor ForegroundColorMsg OSC 10 ; ?
RequestBackgroundColor BackgroundColorMsg OSC 11 ; ?
RequestCursorColor CursorColorMsg OSC 12 ; ?
RequestCursorPosition CursorPositionMsg CSI 6 n
RequestCapability("RGB") CapabilityMsg{Content:"RGB:…"} XTGETTCAP
RequestTerminalVersion TerminalVersionMsg XTVERSION
ReadClipboard ClipboardMsg OSC 52 ; c ; ?
ReadPrimaryClipboard ClipboardMsg OSC 52 ; p ; ?

These commands cooperate with the renderer / runtime to make the round trip transparent. From the model's point of view the only artifact is the response message arriving via Update.

Raw

tea.Raw(any) is the escape hatch for advanced users. The argument is fmt.Sprint-ed and written directly to the terminal. The README and many file comments warn: "Don't use this unless you know what you're doing". It is designed for things like ansi.RequestPrimaryDeviceAttributes that the framework does not expose individually.

Exec

See Exec subprocess. Exec is technically a Cmd but it is described separately because it has its own lifecycle (terminal release / restore).

Integration points

  • The runtime owns dispatch. Commands themselves are pure: they take no arguments and return one Msg.
  • The eventLoop's special-case switch decides whether a Msg is wired into a renderer call (e.g. clearScreenMsg) or to be forwarded to the model.
  • Batch is concurrent and unordered; if you need ordering, use Sequence. Mixing them is fine — the dispatchers handle nested BatchMsg and sequenceMsg.

Entry points for modification

  • A new high-level helper (e.g. tea.Debounce) → put it in commands.go next to Tick/Every. Add a corresponding test in commands_test.go.
  • A new terminal-query command + reply pair → add the request Cmd (e.g. RequestX) and the reply Msg, then handle the request in eventLoop's switch by calling p.execute(ansi.RequestX). The reply will already arrive via translateInputEvent.

Key source files

File Purpose
commands.go Public command helpers
tea.go Dispatch (handleCommands, execBatchMsg, execSequenceMsg)
clipboard.go OSC 52 commands
color.go / cursor.go / screen.go / termcap.go / xterm.go Terminal-query commands
raw.go Escape hatch

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Commands – Bubble Tea wiki | Factory