Skip to content

Bubble Tea Architecture Notes

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

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

Main

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

Bubble Tea

Init initializes our app by kicking off the event loop. We can optionally return any number of cmds.

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

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 Msgs 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 Cmds.

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
}