About
The Bubble Tea library from Charm is a framework for building terminal apps inspired by the Elm architecture.
Key Terms
Model
- should be considered the single source of truth for the entire app
- a struct representation of the current app state
- describes everything that the View needs to render
- user can define an initial model to be passed into
NewProgram()
- after
Init()
, model can only be updated through the Update function via a Msg
View
- responsible for rendering the Text User Interface (TUI) based on the current state of the Model
- uses the Model as its receiver type
- returns a
string
that can be composed with concatenation of other views
Update
- responsible for updating the Model based on a received Msg from either the View or a Cmd
- uses the Model as its receiver type
- takes in a Msg as input, and returns a new Model and any number of Cmds to be performed as side effects
Cmd
- used to perform side effects outside of the Bubble Tea event loop (e.g., http requests, working with files, etc)
- can be triggered by being returned from
Init()
or Update - executed as a goroutine and can produce a Msg that triggers further updates
Msg
- data of any type that represents a user action in the terminal (such as a keypress), a Bubble Tea event, or a Cmd
- sent to the Update to alter the current state of the Model
- defined by either Bubble Tea (e.g.,
tea.KeyMsg
) or user-defined (e.g.,type MyCustomMsg int
)
Here are my diagrams that I've made to help me visualize the flow of data through my Bubble Tea apps. I've added some notes to highlight some key points. They are not exhaustive, but serve as a reference for how different parts of the architecture work together.
Overview of the Bubble Tea architecture
The unidirectional dataflow pattern of Bubble Tea helps simplify our state management and makes it easier to reason about how data flows through our app. As you can see, data always flows in a predictable direction which makes building, maintaining, debugging, and scaling a Bubble Tea app easy to understand.
main.go - Entry point for our app
In the NewProgram()
function, we can optionally pass in an initial model and program options in the form of Bubble Tea commands
// The state of our app upon initialization.
m := initialModel()
// The WithAltScreen gives us full screen support for our app.
p := NewProgram(m, tea.WithAltScreen())
// Run our app.
_, err := p.Run()
// On error, we can log the error and exit with a exit code of 1.
if err != nil {
log.Fatalln(err)
}
Bubble Tea under the hood
Init
initializes our app by kicking off the event loop. We can optionally return any number of cmd
s.
func (m Model) Init() tea.Cmd {
// To run multiple Cmds, we can use `Sequence` (for ordered execution) or `Batch` (concurrent execution).
return tea.Sequence(tea.ClearScreen, tea.ClearScrollArea)
}
Bubble Tea event loop
The Model
can be considered the single source of truth of the entire application and represents the app's current state.
The View
will reflect the state of our model, and we can update the Model
by dispatching a Msg
from the View
or through a Cmd
.
The Update
function essentially subscribes to all of the Msg
s that are sent throughout the app and parses them.
When we are done parsing through the current Msg
, the Update
function will return a new Model
and any number of Cmd
s.
When a new Model
is returned, the View
is then re-rendered automagically.
// Model: app state
type Model struct {
appName string
isLoading bool
currStep uint
}
// View: string representation of the model
func (m Model) View() string {
if m.isLoading {
return "loading..."
}
return m.appName
}
// Update: responds to a msg and returns two things:
// 1. a new model (which then triggers a re-render of the view)
// 2. any number of cmds
func (m Model) Update(msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
switch msg := msg.(type) {
// Handle keypress actions.
case tea.KeyMsg:
switch msg.Type {
case tea.KeyEnter:
m.isLoading = true
cmd = myCommand
return m, cmd
}
// Handle custom defined msgs.
case NextStepMsg:
if msg {
m.currStep++
}
return m, cmd
}
return m, cmd
}