Charm Bracelet
Examples

Exec

What This Example Shows

This example demonstrates how to run external commands (like opening an editor or running shell scripts) from your Bubble Tea app without freezing the interface. It's like having your app call out to other programs while staying responsive.

The key here is tea.ExecProcess - it runs external commands in the background and tells you when they're done, so your UI never gets stuck waiting.

Why Non-Blocking Execution Matters

Picture this: you're using a terminal app and you press a key to open your text editor. Without proper handling, your entire app would freeze until you close the editor. That's pretty annoying, right?

Bubble Tea fixes this with Commands. When you want to run something external (like vim, git, or any shell command), you wrap it in a command that runs separately from your main app.

Long-running tasks like editors, file operations, or network requests will freeze your UI if you don't handle them properly.

Here's what happens:

  • Your app starts the external process
  • The UI stays completely interactive
  • When the external process finishes, you get a message about it
  • You can then decide what to do next

How It Works

The App State

We keep track of two simple things:

type model struct {
    altscreenActive bool  // Are we in alternate screen mode?
    err             error // Did something go wrong?
}

Starting External Commands

When you press 'e', the app figures out which editor to use and starts it:

func openEditor() tea.Cmd {
    editor := os.Getenv("EDITOR")  // Check what editor you prefer
    if editor == "" {
        editor = "vim"  // Default to vim if nothing is set
    }
    c := exec.Command(editor)
    return tea.ExecProcess(c, func(err error) tea.Msg {
        return editorFinishedMsg{err}  // Send a message when done
    })
}

Handling the Results

When the editor closes, your app gets an editorFinishedMsg. If there was an error, it shows it and quits. Otherwise, it just keeps running like nothing happened.

The beauty of this approach is that your app never blocks. Users can interact with it even while external processes are running.

Code Breakdown

The app responds to three keys:

case tea.KeyMsg:
    switch msg.String() {
    case "e":
        return m, openEditor()  // Launch editor
    case "a":
        // Toggle alternate screen
        m.altscreenActive = !m.altscreenActive
        cmd := tea.EnterAltScreen
        if !m.altscreenActive {
            cmd = tea.ExitAltScreen
        }
        return m, cmd
    case "ctrl+c", "q":
        return m, tea.Quit  // Exit the app
    }

The magic happens with tea.ExecProcess:

func openEditor() tea.Cmd {
    editor := os.Getenv("EDITOR")
    if editor == "" {
        editor = "vim"
    }
    c := exec.Command(editor)
    return tea.ExecProcess(c, func(err error) tea.Msg {
        return editorFinishedMsg{err}
    })
}

This creates a command that:

  1. Runs your editor in the background
  2. Calls the callback function when it's done
  3. Sends a message back to your app with any errors

We use a custom message to know when the editor finishes:

type editorFinishedMsg struct{ err error }

// In the Update function:
case editorFinishedMsg:
    if msg.err != nil {
        m.err = msg.err
        return m, tea.Quit
    }

If there's an error, we store it and quit. Otherwise, we just continue.

The view is straightforward - show instructions or errors:

func (m model) View() string {
    if m.err != nil {
        return "Error: " + m.err.Error() + "\n"
    }
    return "Press 'e' to open your EDITOR.\nPress 'a' to toggle the altscreen\nPress 'q' to quit.\n"
}
exec example demonstration

Final Code

main.go
package main

import (
	"fmt"
	"os"
	"os/exec"

	tea "github.com/charmbracelet/bubbletea"
)

type editorFinishedMsg struct{ err error }

func openEditor() tea.Cmd {
	editor := os.Getenv("EDITOR")
	if editor == "" {
		editor = "vim"
	}
	c := exec.Command(editor) //nolint:gosec
	return tea.ExecProcess(c, func(err error) tea.Msg {
		return editorFinishedMsg{err}
	})
}

type model struct {
	altscreenActive bool
	err             error
}

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.KeyMsg:
		switch msg.String() {
		case "a":
			m.altscreenActive = !m.altscreenActive
			cmd := tea.EnterAltScreen
			if !m.altscreenActive {
				cmd = tea.ExitAltScreen
			}
			return m, cmd
		case "e":
			return m, openEditor()
		case "ctrl+c", "q":
			return m, tea.Quit
		}
	case editorFinishedMsg:
		if msg.err != nil {
			m.err = msg.err
			return m, tea.Quit
		}
	}
	return m, nil
}

func (m model) View() string {
	if m.err != nil {
		return "Error: " + m.err.Error() + "\n"
	}
	return "Press 'e' to open your EDITOR.\nPress 'a' to toggle the altscreen\nPress 'q' to quit.\n"
}

func main() {
	m := model{}
	if _, err := tea.NewProgram(m).Run(); err != nil {
		fmt.Println("Error running program:", err)
		os.Exit(1)
	}
}

How is this guide?