Charm Bracelet
Examples

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.Quit

If 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.

Cursor movement 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
    }

Keeps cursor within bounds.

Set choice or quit:

case "enter":
    m.choice = choices[m.cursor]
    return m, tea.Quit

case "ctrl+c", "q", "esc":
    return m, tea.Quit

Selection 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.

result example demonstration

Final Code

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