Suspend
What This Example Shows
This example demonstrates how to handle application lifecycle events in Bubble Tea: suspending, resuming, and interrupting. These features let your app gracefully pause, resume, or quit depending on user input.
Lifecycle management is critical for terminal apps that need to coexist with the shell. Suspending (ctrl+z) hands control back to the shell without killing the process.
Why Lifecycle Events Matter
Imagine you’re running a terminal app and you need to quickly run another command. Instead of quitting the app, you can suspend it with ctrl+z and resume later with fg. Bubble Tea makes this behavior simple and predictable.
- Suspend (
ctrl+z): Pause the app and return control to the shell - Resume (
fg): Bring the app back and restore state - Interrupt (
ctrl+c): Quit cleanly with a specific exit code
If you don’t handle suspend and interrupt properly, your app might leave messy output in the terminal or exit with the wrong status code.
How It Works
The App State
We use a model with two flags:
type model struct {
quitting bool
suspending bool
}quitting: set when the app is about to exitsuspending: set when the app is suspended
Handling Lifecycle Messages
The Update function processes key input and lifecycle messages:
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.ResumeMsg:
m.suspending = false
return m, nil
case tea.KeyMsg:
switch msg.String() {
case "q", "esc":
m.quitting = true
return m, tea.Quit
case "ctrl+c":
m.quitting = true
return m, tea.Interrupt
case "ctrl+z":
m.suspending = true
return m, tea.Suspend
}
}
return m, nil
}Displaying the View
If the app is suspending or quitting, the View returns an empty string, clearing the UI before returning control to the shell:
func (m model) View() string {
if m.suspending || m.quitting {
return ""
}
return "\nPress ctrl-z to suspend, ctrl+c to interrupt, q, or esc to exit\n"
}The key benefit: your app can pause and resume cleanly, just like traditional Unix tools.
Code breakdown
The model holds the quit flag which tells the program whether it should exit.
type model struct {
quitting bool
}The update function listens for the quit message.
When it receives tea.Quit, it sets the quitting flag to true and exits the program.
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg {
case tea.Quit:
return m, tea.Quit
}
return m, nil
}The view renders text to the terminal. If the quit flag is set, it shows a goodbye message; otherwise, it shows the default message.
func (m model) View() string {
if m.quitting {
return "Exiting program. Goodbye!\n"
}
return "Press Ctrl+C to quit.\n"
}The main function initializes the program and starts the Bubble Tea runtime.
func main() {
p := tea.NewProgram(model{})
if _, err := p.Run(); err != nil {
fmt.Println("Error running program:", err)
os.Exit(1)
}
}Final Code
package main
import (
"errors"
"fmt"
"os"
tea "github.com/charmbracelet/bubbletea"
)
type model struct {
quitting bool
suspending bool
}
func (m model) Init() tea.Cmd {
return nil
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.ResumeMsg:
m.suspending = false
return m, nil
case tea.KeyMsg:
switch msg.String() {
case "q", "esc":
m.quitting = true
return m, tea.Quit
case "ctrl+c":
m.quitting = true
return m, tea.Interrupt
case "ctrl+z":
m.suspending = true
return m, tea.Suspend
}
}
return m, nil
}
func (m model) View() string {
if m.suspending || m.quitting {
return ""
}
return "\nPress ctrl-z to suspend, ctrl+c to interrupt, q, or esc to exit\n"
}
func main() {
if _, err := tea.NewProgram(model{}).Run(); err != nil {
fmt.Println("Error running program:", err)
if errors.Is(err, tea.ErrInterrupted) {
os.Exit(130)
}
os.Exit(1)
}
}How is this guide?