Simple Counter
What This Example Shows
This example demonstrates a countdown timer built with Bubble Tea. It highlights the Elm Architecture pattern (Model, View, Update) and introduces timers, messages, and user input handling.
The key here: Bubble Tea apps evolve over time by receiving messages and updating the model. The tick() command sends a new message every second until the countdown ends.
Understanding the Elm Architecture
Bubble Tea is based on the Elm Architecture, which has three parts:
- Model: Holds the application state. In this case, just an integer countdown.
- View: Pure function that renders the UI from the model.
- Update: Handles messages (user input, timers, etc.) and updates the model.
This simple counter is a great starting point for learning how Bubble Tea programs work step by step.
How It Works
Create a Timer Command
type tickMsg time.Time
func tick() tea.Msg {
time.Sleep(time.Second)
return tickMsg{}
}tickMsgsignals that one second has passed.tick()waits one second, then sends atickMsgback into the update loop.
Initialize the Program
func (m model) Init() tea.Cmd {
return tick
}The timer starts immediately when the program launches.
Handle Updates
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":
return m, tea.Quit
case "ctrl+z":
return m, tea.Suspend
}
case tickMsg:
m--
if m <= 0 {
return m, tea.Quit
}
return m, tick
}
return m, nil
}- Keyboard input: quit on
ctrl+corq, suspend withctrl+z. - Timer ticks: decrement the counter, quit if zero, otherwise keep ticking.
Display the View
func (m model) View() string {
return fmt.Sprintf("Hi. This program will exit in %d seconds.\n\nTo quit sooner press ctrl-c, or press ctrl-z to suspend...\n", m)
}The view is a simple string showing how many seconds are left.
Because tick() uses time.Sleep, it blocks for one second before sending the message. This is fine for learning, but in real apps you’ll usually use tea.Tick instead for non-blocking timers.
Code Breakdown
type model intA single integer tracks how many seconds remain.
type tickMsg time.Time
func tick() tea.Msg {
time.Sleep(time.Second)
return tickMsg{}
}Waits a second, then sends tickMsg.
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":
return m, tea.Quit
case "ctrl+z":
return m, tea.Suspend
}
case tickMsg:
m--
if m <= 0 {
return m, tea.Quit
}
return m, tick
}
return m, nil
}Handles quitting, suspending, or decrementing the countdown.
func (m model) View() string {
return fmt.Sprintf("Hi. This program will exit in %d seconds.\n\nTo quit sooner press ctrl-c, or press ctrl-z to suspend...\n", m)
}Shows the countdown timer to the user.
func main() {
logfilePath := os.Getenv("BUBBLETEA_LOG")
if logfilePath != "" {
if _, err := tea.LogToFile(logfilePath, "simple"); err != nil {
log.Fatal(err)
}
}
p := tea.NewProgram(model(5))
if _, err := p.Run(); err != nil {
log.Fatal(err)
}
}Starts with a countdown of 5 seconds and optional logging.
The View
Every second, the countdown decreases until the app exits:
Final Code
package main
import (
"fmt"
"log"
"os"
"time"
tea "github.com/charmbracelet/bubbletea"
)
type model int
type tickMsg time.Time
func tick() tea.Msg {
time.Sleep(time.Second)
return tickMsg{}
}
func (m model) Init() tea.Cmd {
return tick
}
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":
return m, tea.Quit
case "ctrl+z":
return m, tea.Suspend
}
case tickMsg:
m--
if m <= 0 {
return m, tea.Quit
}
return m, tick
}
return m, nil
}
func (m model) View() string {
return fmt.Sprintf("Hi. This program will exit in %d seconds.\n\nTo quit sooner press ctrl-c, or press ctrl-z to suspend...\n", m)
}
func main() {
logfilePath := os.Getenv("BUBBLETEA_LOG")
if logfilePath != "" {
if _, err := tea.LogToFile(logfilePath, "simple"); err != nil {
log.Fatal(err)
}
}
p := tea.NewProgram(model(5))
if _, err := p.Run(); err != nil {
log.Fatal(err)
}
}How is this guide?