Charm Bracelet
Examples

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

Define the Model

type model int

The model is just an integer representing the remaining seconds.

Create a Timer Command

type tickMsg time.Time

func tick() tea.Msg {
    time.Sleep(time.Second)
    return tickMsg{}
}
  • tickMsg signals that one second has passed.
  • tick() waits one second, then sends a tickMsg back 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+c or q, suspend with ctrl+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 int

A 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:

simple counter example demonstration

Final Code

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