Charm Bracelet
Examples

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 exit
  • suspending: 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

main.go
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?