Result
What This Example Shows
This example demonstrates how to build a simple command-line interface (TUI) that prompts a user for input, captures their selection, and returns the final value to the calling process after the application exits. This is a common pattern for using a TUI to gather configuration or choices that are then used by a script or another program.
The key here is that Program.Run() returns the final model state after tea.Quit, allowing you to access user selections post-exit.
Understanding Returning Results in Bubble Tea
Bubble Tea's Program.Run() method blocks until the program quits via tea.Quit and then returns the final model. This enables inspecting the model's state after the TUI closes to retrieve data like user choices.
This pattern is useful for integrating TUIs into larger scripts or programs where the TUI collects input and the main process uses it afterward.
This is perfect for creating modular TUIs that act as input gatherers, seamlessly passing data back without side effects like printing during runtime.
Here's what happens:
- The TUI presents choices and handles navigation/selection
- On selection, it sets the choice in the model and quits
- After Run() returns, the main function checks the final model and prints the choice
How It Works
Define the Model and Choices
The model tracks cursor and choice; choices are predefined:
var choices = []string{"Taro", "Coffee", "Lychee"}
type model struct {
cursor int
choice string
}cursor for highlighting, choice for final selection.
Handle Navigation
In Update, move cursor on up/down keys with wrapping:
case "down", "j":
m.cursor++
if m.cursor >= len(choices) {
m.cursor = 0
}
case "up", "k":
m.cursor--
if m.cursor < 0 {
m.cursor = len(choices) - 1
}Handle Selection and Quit
On enter, set choice and quit; also handle exit keys:
case "enter":
m.choice = choices[m.cursor]
return m, tea.Quit
case "ctrl+c", "q", "esc":
return m, tea.QuitIf the user quits without selecting, the choice remains empty; always check the final model to avoid using unset values.
Code Breakdown
Simple model for state:
var choices = []string{"Taro", "Coffee", "Lychee"}
type model struct {
cursor int
choice string
}cursor starts at 0, choice is initially empty.
Initialize and run the program:
func main() {
p := tea.NewProgram(model{})
m, err := p.Run()
if err != nil {
fmt.Println("Oh no:", err)
os.Exit(1)
}
if m, ok := m.(model); ok && m.choice != "" {
fmt.Printf("\n---\nYou chose %s!\n", m.choice)
}
}
func (m model) Init() tea.Cmd {
return nil
}Run() returns final model; assert and print choice.
Handle messages:
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "q", "esc":
// Quit
case "enter":
// Select
case "down", "j":
// Down
case "up", "k":
// Up
}
}
return m, nil
}Switches on key presses.
Set choice or quit:
case "enter":
m.choice = choices[m.cursor]
return m, tea.Quit
case "ctrl+c", "q", "esc":
return m, tea.QuitSelection updates model before quitting.
The View
Render choices with cursor indicator:
func (m model) View() string {
s := strings.Builder{}
s.WriteString("What kind of Bubble Tea would you like to order?\n\n")
for i := 0; i < len(choices); i++ {
if m.cursor == i {
s.WriteString("(•) ")
} else {
s.WriteString("( ) ")
}
s.WriteString(choices[i])
s.WriteString("\n")
}
s.WriteString("\n(press q to quit)\n")
return s.String()
}Uses StringBuilder for efficient rendering.
Final Code
package main
import (
"fmt"
"os"
"strings"
tea "github.com/charmbracelet/bubbletea"
)
var choices = []string{"Taro", "Coffee", "Lychee"}
type model struct {
cursor int
choice string
}
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 "ctrl+c", "q", "esc":
return m, tea.Quit
case "enter":
// Send the choice on the channel and exit.
m.choice = choices[m.cursor]
return m, tea.Quit
case "down", "j":
m.cursor++
if m.cursor >= len(choices) {
m.cursor = 0
}
case "up", "k":
m.cursor--
if m.cursor < 0 {
m.cursor = len(choices) - 1
}
}
}
return m, nil
}
func (m model) View() string {
s := strings.Builder{}
s.WriteString("What kind of Bubble Tea would you like to order?\n\n")
for i := 0; i < len(choices); i++ {
if m.cursor == i {
s.WriteString("(•) ")
} else {
s.WriteString("( ) ")
}
s.WriteString(choices[i])
s.WriteString("\n")
}
s.WriteString("\n(press q to quit)\n")
return s.String()
}
func main() {
p := tea.NewProgram(model{})
// Run returns the model as a tea.Model.
m, err := p.Run()
if err != nil {
fmt.Println("Oh no:", err)
os.Exit(1)
}
// Assert the final tea.Model to our local model and print the choice.
if m, ok := m.(model); ok && m.choice != "" {
fmt.Printf("\n---\nYou chose %s!\n", m.choice)
}
}How is this guide?