I’ve been dabbling a bit with BubbleTea a terminal user interface library written in go. It has been the bane of my existence for the past few months and I’ve been trying to figure out what about it has made it so difficult. I think the reason I find it so difficult and annoying to pick up is that there really isn’t any well written guide for how to get started.
First of all, it’s not a monolithic repo which is fine, but it has the drawback of making getting started very difficult.
The main repo does reference some of the various libraries that you should be using. You can find that list here. I also pasted it below for reference in case the README changes.
Bubbles: Common Bubble Tea components such as text inputs, viewports, spinners and so on
Lip Gloss: Style, format and layout tools for terminal applications
Harmonica: A spring animation library for smooth, natural motion
BubbleZone: Easy mouse event tracking for Bubble Tea components
ntcharts: A terminal charting library built for Bubble Tea and Lip Gloss
Termenv: Advanced ANSI styling for terminal applications
Reflow: Advanced ANSI-aware methods for working with text
A typical Bubbletea program will use at the very least bubbles (aka components) and lipgloss (aka styling) while adding any of the other libraries above as needed/desired.
What bubble tea does have as far as documentation is:
the golang docs of course.
An examples directory which can be helpful but it’s also feels like one of those “Well it’s open source read the code and figure it out” that used to troll the OSS communities ages ago.
Lastly a Youtube channel. Though I appreciate it, it really doesn’t replace well written documentation. It’s also inherently harder to maintain and keep up to date. Most of those videos have become fair out of date.
There really isn’t a guide I could find that’s a progressively explains ELM architecture, concepts and then keeps on adding building blocks. Most of the tutorials I’ve found are one offs written by individuals that solve a problem they had. Example: I made a todo list. If you have a use case that’s not a hello world, but also you’re not trying to write a file manager there seems very limited content that covers that simple middle ground. The ‘patterns’ that need to be used is also not very clear. I’m going to try to write a series of my journey to capture all of this. For now we’ll limit it to the hello world examples and explain certain concepts.
So here’s my attempt at explaining Bubble as I learn how to write bubbletea apps. Please join me on my journey.
BubbleTea uses ELM Architecture. It’s typically more commonly used for web browsers and UI patterns but essentially the entire TUI representation is sent around as a string. Everything is a string and there’s tooling built around it. We have events that we react to, those would be mouse and keyboard clicks that modify the model and sent an update string representation on what to display. Basically if you are used to writing GUI applications with windows layouts and such, none of that is applicable and you’re starting from scratch. Throw all your acquired knowledge away and embrace the ELM.
Everything in bubbletea is based around a model. aka. a struct in golang. There three methods that we need to be aware of:
Init() // This is a method that is called once and initializes the state of the struct. The struct can contain any number of member variables to help you track the state of the program, build up an entity, API call or whatever the purpose of the CLI is.
Update(msg tea.Msg) (tea.Model, tea.Cmd) // this method is invoked as a reaction to an event. Typically things like keyboard presses, mouse events etc are handled here and the “model” aka string representation of the UI is updated.
View() string // draws the model
Once we have a model, it’s passed to bubbletea that creates the ELM loop using our program. Let’s start with a main.go
package main
import (
tea "github.com/charmbracelet/bubbletea"
)
func main() {
p := tea.NewProgram(
NewModel("Hello World"),
)
if _, err := p.Run(); err != nil {
panic(err)
}
}
Then let’s create the models:
package main
import (
"fmt"
tea "github.com/charmbracelet/bubbletea"
)
type helloModel struct {
greeting string
}
func NewModel(text string) helloModel {
return helloModel{greeting: text}
}
func (s helloModel) Init() tea.Cmd { return nil }
func (s helloModel) View() string {
return fmt.Sprintf("%s\n\nPress Ctrl+C to exit", s.greeting)
}
func (s helloModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg.(type) {
case tea.KeyMsg:
//String version
//key := msg.(tea.KeyMsg).String()
//switch key {
//case "ctrl+c", "esc":
// return s, tea.Quit
//}
//KeyType. String version is more versatile, mainly since not every key combination is available.
key := msg.(tea.KeyMsg)
switch key.Type {
case tea.KeyCtrlC, tea.KeyEsc:
return s, tea.Quit
}
}
return s, nil
}
Then running the go:
go run .
Okay, now what? Well now I finish my coffee and pickup and start a new draft. I know it’s a lot of boiler plate that basically succeeded in printing a “Hello world”. Next time we’ll add state and changing behavior based on keypresses. You are somewhat doing that reacting to Ctrl+C to exit the program.