Charm Bracelet
Examples

Fullscreen

What This Example Shows

This example demonstrates how to create a fullscreen terminal app that takes over the entire screen, just like when you open vim or htop. It shows a simple countdown timer that runs in "alternate screen mode" - your app gets a clean slate to work with.

The key here is tea.WithAltScreen() - it gives your app the entire terminal window and restores everything back to normal when you exit.

Understanding the Alternate Screen Buffer

Think of your terminal as having two screens: your normal command line where you type commands and see output, and a hidden "alternate screen" that apps can use for full interfaces.

When you open programs like vim, less, or htop, they switch to this alternate screen. That's why when you quit them, you're right back where you started - your command history and previous output are still there, completely untouched.

This is perfect for creating immersive apps that don't mess with the user's terminal session. When they exit your app, it's like it was never there.

Here's what happens:

  • Your app starts and takes over the full terminal
  • Users see only your interface (no command prompt, no previous output)
  • When they quit, everything goes back to exactly how it was before

How It Works

Enable Alternate Screen

Tell Bubble Tea you want to use the full screen:

p := tea.NewProgram(model(5), tea.WithAltScreen())

The tea.WithAltScreen() option does all the heavy lifting - switching to alternate screen on start and back to normal on exit.

Create a Timer

This example uses a simple countdown timer to show something happening:

func tick() tea.Cmd {
    return tea.Tick(time.Second, func(t time.Time) tea.Msg {
        return tickMsg(t)  // Send a message every second
    })
}

Handle the Countdown

Each second, decrement the counter and check if we should exit:

case tickMsg:
    m--  // Count down
    if m <= 0 {
        return m, tea.Quit  // Time's up!
    }
    return m, tick()  // Keep going

The alternate screen buffer is supported by most modern terminals, but very old or minimal terminal emulators might not support it.

Code Breakdown

The app uses a super simple model - just an integer for the countdown:

type model int        // The countdown number
type tickMsg time.Time // Message sent every second

func main() {
    p := tea.NewProgram(model(5), tea.WithAltScreen())
    // Start with 5 seconds, enable fullscreen
    if _, err := p.Run(); err != nil {
        log.Fatal(err)
    }
}

Starting with model(5) means we count down from 5 seconds.

The Init function kicks off the timer right away:

func (m model) Init() tea.Cmd {
    return tick()  // Start the countdown immediately
}

This returns a command that will send the first tick message after one second.

The heart of the countdown logic:

func (m model) Update(message tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := message.(type) {
    case tickMsg:
        m--              // Subtract one second
        if m <= 0 {
            return m, tea.Quit  // Exit when we hit zero
        }
        return m, tick()     // Schedule the next tick
    }
    return m, nil
}

Each tick decrements the counter and schedules the next one, until we reach zero.

Users can exit early if they want:

case tea.KeyMsg:
    switch msg.String() {
    case "q", "esc", "ctrl+c":
        return m, tea.Quit  // Let users quit anytime
    }

This gives users control - they don't have to wait for the full countdown.

The View

The view is simple but shows the current countdown state:

func (m model) View() string {
    return fmt.Sprintf("\n\n     Hi. This program will exit in %d seconds...", m)
}

Since we're in fullscreen mode, this message appears centered in a clean terminal window.

fullscreen example demonstration

Final Code

main.go
package main

import (
	"fmt"
	"log"
	"time"

	tea "github.com/charmbracelet/bubbletea"
)

type model int

type tickMsg time.Time

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

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

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

	case tickMsg:
		m--
		if m <= 0 {
			return m, tea.Quit
		}
		return m, tick()
	}

	return m, nil
}

func (m model) View() string {
	return fmt.Sprintf("\n\n     Hi. This program will exit in %d seconds...", m)
}

func tick() tea.Cmd {
	return tea.Tick(time.Second, func(t time.Time) tea.Msg {
		return tickMsg(t)
	})
}

How is this guide?