21 March 2023

Commands in Bubble Tea

By Bashbunni

Because of its Elm-inspired roots, Bubble Tea has a special approach to asynchronous operations: commands.

Commands are a fundamental part of Bubble Tea and are present whenever I/O needs to happen. There may built-in commands you’re familiar with already such as the tea.Quit, tea.Tick, and so on.

Also, if you’re just getting started with Bubble Tea we recommend checking out the Bubble Tea Commands tutorial. It is worth the read.

Rules of thumb

There three things to keep in mind with regard to asynchronous stuff in Bubble Tea:

  • Use commands for all I/O. By doing so your program will stay responsive, snappy, and maintainable. Even something as simple as reading a file from disk could cause a small lock up in your program, and commands are build to handle such cases beautifully.
  • Only use commands for I/O. Sometimes it’s tempting to use a command simply to send a message to another part of the program, however due to the nature of the way data flows in Bubble Tea this is never actually necessary.
  • Never use goroutines within a Bubble Tea program. Bubble Tea works best when you use commands and messages for communication.

The basics

Okay, so how would we write our own commands?

A Cmd is simply an alias for func() tea.Msg, so it’s just a function that returns a message. A command could be something as simple as:

type helloMsg string

func waitASec() tea.Msg {
	time.Sleep(time.Second)
	return helloMsg("Hi, there!")
}

But how would you respond to such a command? You’d match on it in your update method.

func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case helloMsg:
		// We caught our message like a Pokémon!
		// From here you could save the output to the model
		// to display it later in your view.
		m.greeting = msg;
	default:
		return m, nil
	}
}

Multiple commands

Sometimes you want to fire off more than one command at once. For that, use tea.Batch.

func choresCmd() tea.Msg {
	return tea.Batch(getTheLaundry, eatDinner, petTheCats)
}

Commands with arguments

Okay, so what if you want to make a command that takes an argument? Since Cmd is a func() tea.Msg, it can’t take an argument. So what do you do in that case? Well, you make a function that returns a command.

func getUser(id) tea.Cmd {
	return func() tea.Msg {
		user, _ := api.GetUserByID(id)
		return gotUser{user}
	}
}

In your Update function it would look something like this:

return m, getUser(88)

Initial commands

Sometimes you’ll want to fire off a command right as your Bubble Tea program starts without waiting for user input. For that, use Model.Init:

func (m Model) Init() tea.Msg {
	return fetchUsers
}

Or, for multiple commands:

func (m Model) Init() tea.Msg {
	return tea.Batch(
		fetchUsers,
		spinner.Tick,
	)
}

APIs calls from Bubble Tea

A lot of the time you’ll want to make API calls from Bubble Tea. Commands are great for this.

func getUser(id int) tea.Cmd {
	return func() tea.Msg {
		res, err := http.Get(fmt.Sprintf("http://api.example.com/users?id=%d", id))
		if err != nil {
			return apiErrMsg{err}
		}
		// ...do unmarshaling and stuff...
		return fetchedUserMsg(u)
	}
}

func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.KeyMsg:
		switch msg.String() {
		case "enter":
			// Make our API request
			return m, getUser(m.userID)
		}
	case fetchedUserMsg:
		// Here's our API response
	case apiErrMsg:
		// Oh no, an API error!
	default:
		return m, nil
	}
}

Injecting messages from outside the program

Commands work great from within a Bubble Tea program, but what if you want to send messages to Update from outside the program? Enter Program.Send(). It’s as simple as p.Send(someMsg{}):

p := tea.NewProgram(model)

// Later...
p.Send(someMsg{})

For details check out the full example and Program.Send in the docs.

Still have questions?

Let us know in Discord or in GitHub Discussions.

Learn More

EOF

Read this post in your terminal with Glow:

glow -p https://charm.sh/blog/commands-in-bubbletea.md Copied!

By Bashbunni

21 March 2023

Lets chat!

Have a question about a command line thing you’re building? Got an idea for a new feature? Just wanna hang out? You’re always welcome in the Charm Discord.