Intro

Bubbletea is a framework with a philosophy based on The Elm Architecture: Making it simple, it breaks into three parts:

  • Model: the state of your application
  • View: a way to turn your state into something to display
  • Update: a way to update your state based on messages

The framework’s runtime manages everything else: from messages orchestration to low-level rendering details.

Example

Let’s say you want to create the classic to-do list:

  • your model will be a struct holding a list of tasks, and a flag to mark them “Done”

    type model struct {
        tasks  []string // items on the to-do list
        done   []bool   // which to-do items are done
    }

  • your view will be a function that takes the model and return a string representation of the task list

    func (m model) View() string {
        var s strings.Builder
    
        // Iterate over our choices
        for i, tasks := range m.tasks {
    
            // Is this task done?
            if m.done[i] {
    			s.WriteString("[x] ")
    		} else {
    			s.WriteString("[ ] ")
    		}
    		s.WriteString(tasks[i]+"\n")
        }
        return s.String()
    }

  • your update will be a function that takes a message that comes from the framework and reacts to it, for example a keypress, a mouse click or a window resize. Check the docs for details.

    func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    	switch msg := msg.(type) {
    		// here you need to process events from the framework
    		// and return an updated model plus optionally a new Command to execute
    	}
    }

  • As a final step, you only need to pass the model to the framework:

    func main() {
        p := tea.NewProgram(model{})
        if _, err := p.Run(); err != nil {
            fmt.Printf("There's been an error: %v", err)
            os.Exit(1)
        }
    }

Something fun

Instead of the boring to-do list, as another example I implemented the classic “bouncing ball” with the walls that can be resizeable; you can see the program reacts to the window resize and acts accordingly.

bouncing ball

Full size video here

The source code is rather simple:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
package main

import (
	"fmt"
	"os"
	"strings"
	"time"

	tea "github.com/charmbracelet/bubbletea"
)

type model struct {
	width  int
	height int
	x      int
	y      int
	dx     int
	dy     int
}

type tickMsg time.Time

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

// Custom message tied to a timer
func tickCmd() tea.Cmd {
	return tea.Tick(time.Millisecond*20, func(t time.Time) tea.Msg {
		return tickMsg(t)
	})
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {

	case tea.WindowSizeMsg:
		m.height = msg.Height
		m.width = msg.Width

	case tickMsg:
		x1 := m.x + m.dx
		y1 := m.y + m.dy
		if x1 < 0 || x1 > m.width {
			m.dx = -m.dx
		}
		if y1 < 0 || y1 > m.height {
			m.dy = -m.dy
		}
		m.x += m.dx
		m.y += m.dy
		return m, tickCmd()

	case tea.KeyMsg:
		switch msg.String() {
		case "ctrl+c", "q":
			return m, tea.Quit
		}
	}
	// Return the updated model to the Bubble Tea runtime for processing.
	return m, nil
}

func (m model) View() string {
	var s strings.Builder
	for i := 0; i < m.y; i++ {
		s.WriteString("\n")
	}
	s.WriteString(strings.Repeat(" ", m.x))
	s.WriteString("o")
	return s.String()
}

func main() {
	p := tea.NewProgram(model{x: 1, y: 1, dx: 1, dy: 1}, tea.WithAltScreen())
	if _, err := p.Run(); err != nil {
		fmt.Printf("There's been an error: %v", err)
		os.Exit(1)
	}
}

The only big difference here is a custom message type, used to send periodic events to the application in order to update itself without user interaction. Sounds like one can also write some games 😉 !

Of course you can find lots of examples in the library’s repository.

Wrapping up

With Bubbletea I can also recommend some ideal “companion” libraries, all from https://github.com/charmbracelet/:

  • Bubbles for ready made “components”
  • Lipgloss for colorization, layout and styling, more or less “CSS for the terminal”

Have fun!