Charm Bracelet
Examples

HTTP Request

What This Example Shows

This example demonstrates how to handle I/O operations, such as HTTP requests, within a Bubble Tea application without blocking the user interface. It shows a simple GET request to check a server's status and display the result asynchronously.

The key here is using Bubble Tea's command architecture (tea.Cmd) to perform long-running tasks like HTTP requests outside the main event loop, keeping the UI responsive.

Understanding I/O in Terminal User Interfaces

In Terminal User Interfaces (TUIs), responsiveness is crucial since they run in a single-threaded event loop. Performing blocking I/O operations, like HTTP requests, directly in the update loop would freeze the app, making it unresponsive to user input.

Bubble Tea solves this with asynchronous commands: functions that run in separate goroutines and return messages with results, allowing the app to update state without blocking.

This approach is perfect for creating responsive apps that handle network operations gracefully, ensuring users can interact (e.g., quit) even during long tasks.

Here's what happens:

  • The app starts and initiates an HTTP request via a command
  • The UI shows a "Checking..." message while the request runs in the background
  • When complete, a message updates the model with the status or error, and the view reflects it

How It Works

Define the Model

The model holds the request state:

type model struct {
    status int
    err    error
}

It tracks the HTTP status code or any error.

Create Custom Messages

Define messages for success and error:

type statusMsg int

type errMsg struct{ error }

These will be returned by the command.

Perform the HTTP Request

The command handles the GET request:

func checkServer() tea.Msg {
    c := &http.Client{Timeout: 10 * time.Second}
    res, err := c.Get(url)
    if err != nil {
        return errMsg{err}
    }
    defer res.Body.Close()
    return statusMsg(res.StatusCode)
}

It runs asynchronously and returns a message.

HTTP requests depend on network conditions; always handle timeouts and errors to avoid indefinite hangs.

Code Breakdown

The app uses a simple model to store the response state:

type model struct {
    status int
    err    error
}

const url = "https://charm.sh/"

status defaults to 0, and err is nil initially.

Custom messages for command results:

type statusMsg int

type errMsg struct{ error }

func (e errMsg) Error() string { return e.error.Error() }

statusMsg carries the status code; errMsg wraps errors.

The command performs the request:

func checkServer() tea.Msg {
    c := &http.Client{
        Timeout: 10 * time.Second,
    }
    res, err := c.Get(url)
    if err != nil {
        return errMsg{err}
    }
    defer res.Body.Close()

    return statusMsg(res.StatusCode)
}

It uses a timed-out client for safety.

Start the program and init the command:

func main() {
    p := tea.NewProgram(model{})
    if _, err := p.Run(); err != nil {
        log.Fatal(err)
    }
}

func (m model) Init() tea.Cmd {
    return checkServer
}

Init triggers the request immediately.

Handle messages from the command:

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case statusMsg:
        m.status = int(msg)
        return m, tea.Quit

    case errMsg:
        m.err = msg
        return m, nil

    default:
        return m, nil
    }
}

Updates state and quits on success.

Allow quitting during the request:

case tea.KeyMsg:
    switch msg.String() {
    case "q", "ctrl+c", "esc":
        return m, tea.Quit
    default:
        return m, nil
    }

This keeps the app responsive.

The View

The view renders based on the current state:

func (m model) View() string {
    s := fmt.Sprintf("Checking %s...", url)
    if m.err != nil {
        s += fmt.Sprintf("something went wrong: %s", m.err)
    } else if m.status != 0 {
        s += fmt.Sprintf("%d %s", m.status, http.StatusText(m.status))
    }
    return s + "\n"
}

It shows progress, errors, or results.

http example demonstration

Final Code

main.go
package main

import (
	"fmt"
	"log"
	"net/http"
	"time"

	tea "github.com/charmbracelet/bubbletea"
)

const url = "https://charm.sh/"

type model struct {
	status int
	err    error
}

type statusMsg int

type errMsg struct{ error }

func (e errMsg) Error() string { return e.error.Error() }

func main() {
	p := tea.NewProgram(model{})
	if _, err := p.Run(); err != nil {
		log.Fatal(err)
	}
}

func (m model) Init() tea.Cmd {
	return checkServer
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.KeyMsg:
		switch msg.String() {
		case "q", "ctrl+c", "esc":
			return m, tea.Quit
		default:
			return m, nil
		}

	case statusMsg:
		m.status = int(msg)
		return m, tea.Quit

	case errMsg:
		m.err = msg
		return m, nil

	default:
		return m, nil
	}
}

func (m model) View() string {
	s := fmt.Sprintf("Checking %s...", url)
	if m.err != nil {
		s += fmt.Sprintf("something went wrong: %s", m.err)
	} else if m.status != 0 {
		s += fmt.Sprintf("%d %s", m.status, http.StatusText(m.status))
	}
	return s + "\n"
}

func checkServer() tea.Msg {
	c := &http.Client{
		Timeout: 10 * time.Second,
	}
	res, err := c.Get(url)
	if err != nil {
		return errMsg{err}
	}
	defer res.Body.Close() // nolint:errcheck

	return statusMsg(res.StatusCode)
}

How is this guide?