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:
- Runs your editor in the background
- Calls the callback function when it's done
- 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"
}
Final Code
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?