Charm Bracelet
Examples

Focus Blur

What This Example Shows

This example shows how your Bubble Tea app can tell when the terminal window is focused or not. Think of it like knowing when someone is looking at your app versus when they've clicked away to do something else.

This is super useful for apps that need to pause animations, stop polling data, or change their behavior when users aren't actively looking at them.

Understanding Terminal Focus Events

You know how your browser tabs dim when you switch away from them? Terminal applications can do something similar by listening for focus events.

Here's what happens:

  • Focus Event: Your terminal window becomes active (user clicks on it, switches to it, etc.)
  • Blur Event: Your terminal window becomes inactive (user clicks somewhere else, switches to another app, etc.)

Not all terminal emulators support these events. Modern ones like iTerm2, Windows Terminal, and most Linux terminals do, but older or simpler terminals might not.

This is particularly handy for:

  • Pausing resource-intensive operations when users aren't watching
  • Showing different status indicators based on focus
  • Being a good citizen and not hogging CPU when hidden

How It Works

Enable Focus Reporting

First, you need to tell Bubble Tea that you want to know about focus changes:

p := tea.NewProgram(model{
    focused:   true,   // Start assuming we're focused
    reporting: true,   // Start with reporting enabled
}, tea.WithReportFocus())  // This is the magic line

Track the State

Keep track of whether your app is focused and whether you want to report changes:

type model struct {
    focused   bool  // Is the terminal focused right now?
    reporting bool  // Should we care about focus changes?
}

Handle Focus Messages

When focus changes happen, Bubble Tea sends you messages about it:

case tea.FocusMsg:
    m.focused = true   // Terminal just got focused
case tea.BlurMsg:
    m.focused = false  // Terminal just lost focus

The app can toggle focus reporting on and off by pressing 't', which is great for testing and demonstrating the feature.

Code Breakdown

The app starts with focus reporting enabled:

func main() {
    p := tea.NewProgram(model{
        focused:   true,    // Assume we start focused
        reporting: true,    // Start with reporting on
    }, tea.WithReportFocus())  // Enable focus event reporting
    
    if _, err := p.Run(); err != nil {
        log.Fatal(err)
    }
}

The tea.WithReportFocus() option is what makes the magic happen - without it, you won't get any focus events.

The app listens for two specific message types:

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.FocusMsg:
        m.focused = true    // Window gained focus
    case tea.BlurMsg:
        m.focused = false   // Window lost focus
    case tea.KeyMsg:
        // Handle key presses...
    }
    return m, nil
}

These messages are sent automatically by Bubble Tea when the terminal's focus state changes.

Users can interact with the demo in two ways:

case tea.KeyMsg:
    switch msg.String() {
    case "t":
        m.reporting = !m.reporting  // Toggle reporting on/off
    case "ctrl+c", "q":
        return m, tea.Quit          // Exit the app
    }

The 't' key lets you toggle focus reporting to see the difference when it's enabled vs disabled.

The view shows different messages based on the current state:

func (m model) View() string {
    s := "Hi. Focus report is currently "
    if m.reporting {
        s += "enabled"
    } else {
        s += "disabled"
    }
    s += ".\n\n"

    if m.reporting {
        if m.focused {
            s += "This program is currently focused!"
        } else {
            s += "This program is currently blurred!"
        }
    }
    return s + "\n\nTo quit sooner press ctrl-c, or t to toggle focus reporting...\n"
}

Final Code

main.go
package main

import (
	"log"

	tea "github.com/charmbracelet/bubbletea"
)

func main() {
	p := tea.NewProgram(model{
		// assume we start focused...
		focused:   true,
		reporting: true,
	}, tea.WithReportFocus())
	if _, err := p.Run(); err != nil {
		log.Fatal(err)
	}
}

type model struct {
	focused   bool
	reporting bool
}

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.FocusMsg:
		m.focused = true
	case tea.BlurMsg:
		m.focused = false
	case tea.KeyMsg:
		switch msg.String() {
		case "t":
			m.reporting = !m.reporting
		case "ctrl+c", "q":
			return m, tea.Quit
		}
	}

	return m, nil
}

func (m model) View() string {
	s := "Hi. Focus report is currently "
	if m.reporting {
		s += "enabled"
	} else {
		s += "disabled"
	}
	s += ".\n\n"

	if m.reporting {
		if m.focused {
			s += "This program is currently focused!"
		} else {
			s += "This program is currently blurred!"
		}
	}
	return s + "\n\nTo quit sooner press ctrl-c, or t to toggle focus reporting...\n"
}

How is this guide?