If you are looking for a way to create rich and interactive text-based user interfaces in Go, look no further than the Bubble Tea library. This powerful and flexible library simplifies the process of building TUI (Text User Interface) applications, enabling you to create applications that are both visually appealing and highly functional.
What is Bubble Tea?
Bubble Tea is a Go library developed by Charm_. It is inspired by The Elm Architecture, making it easy to manage state and update your UI in a predictable manner. The library is ideal for creating a variety of applications, including dashboards, command-line tools, and games.
Key Features
- Elm-Inspired Architecture: Manage your application’s state in a structured and predictable way.
- Flexible Rendering: Use the Bubble Tea rendering engine to create dynamic and interactive interfaces.
- Concurrent Programming: Take advantage of Go’s concurrency model to build responsive applications.
- Cross-Platform: Works seamlessly across different operating systems.
Getting Started
I will use simple application that I created for demo purpose.
It is a simple RSS reader that fetches the latest news from a feed and displays it in the terminal. The application uses Bubble Tea to create an interactive TUI that allows the user to navigate through the news items and read the full content of each article.
Main app run model and give TUI data. Project have all components related to TUI in tui
package.
package main
import (
"fmt"
"log"
"os"
"github.com/abtris/rss-bubbletea/tui"
tea "github.com/charmbracelet/bubbletea"
"github.com/mmcdole/gofeed"
)
func main() {
file, _ := os.Open("data/podcast.xml")
defer file.Close()
fp := gofeed.NewParser()
feed, err := fp.Parse(file)
if err != nil {
log.Fatal("parse feed failed", err)
}
model, err := tui.NewModel(feed)
if err != nil {
log.Fatal("create model failed", err)
}
if len(os.Getenv("DEBUG")) > 0 {
f, err := tea.LogToFile("debug.log", "debug")
if err != nil {
fmt.Println("fatal:", err)
os.Exit(1)
}
defer f.Close()
}
p := tea.NewProgram(model, tea.WithAltScreen(), tea.WithMouseCellMotion())
if err := p.Start(); err != nil {
log.Fatal("start failed: ", err)
}
}
model tui/model.go
using list component from Bubbles component library.
package tui
import (
"log"
md "github.com/JohannesKaufmann/html-to-markdown"
"github.com/charmbracelet/bubbles/list"
"github.com/mmcdole/gofeed"
)
type model struct {
list list.Model
choice string
content string
detail bool
quitting bool
}
const width = 80
const height = 40
const title = "RSS Reader"
func NewModel(data *gofeed.Feed) (*model, error) {
var items []list.Item
converter := md.NewConverter("", true, nil)
for _, rssItem := range data.Items {
markdown, err := converter.ConvertString(rssItem.Description)
if err != nil {
log.Println("Convert to markdown", err)
}
i := item{
title: rssItem.Title,
desc: "Published at " + rssItem.Published + "\n\n" + markdown,
}
items = append(items, i)
}
l := list.New(items, list.NewDefaultDelegate(), width, height)
l.Title = title
return &model{
list: l,
choice: "",
content: "",
detail: false,
}, nil
}
interaction with interface is covered by tui/update.go
package tui
import (
tea "github.com/charmbracelet/bubbletea"
)
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.list.SetWidth(msg.Width)
return m, nil
case tea.KeyMsg:
switch keypress := msg.String(); keypress {
case "q", "ctrl+c":
m.quitting = true
return m, tea.Quit
case "enter":
i, ok := m.list.SelectedItem().(item)
if ok {
m.detail = true
m.choice = string(i.title)
m.content = string(i.desc)
}
return m, nil
case "b":
if m.detail {
m.choice = ""
m.content = ""
}
case "p":
if m.detail {
changeIndex := m.list.Index() + 1
if changeIndex <= 0 {
changeIndex = 0
}
m.list.Select(changeIndex)
i, ok := m.list.SelectedItem().(item)
if ok {
m.choice = string(i.title)
m.content = string(i.desc)
}
}
case "n":
if m.detail {
changeIndex := m.list.Index() - 1
maxLength := len(m.list.Items())
if changeIndex > (maxLength - 1) {
changeIndex = maxLength - 1
}
m.list.Select(changeIndex)
i, ok := m.list.SelectedItem().(item)
if ok {
m.choice = string(i.title)
m.content = string(i.desc)
}
}
}
}
var cmd tea.Cmd
m.list, cmd = m.list.Update(msg)
return m, cmd
}
and view tui/view.go
take care about rendering. I’m using glamour for rendering markdown content.
package tui
import (
"log"
"github.com/charmbracelet/glamour"
)
func (m model) View() string {
var s string
if len(m.choice) > 0 {
s += "# " + title
s += "\n## " + m.choice
s += "\n\n"
s += m.content
renderer, err := glamour.NewTermRenderer(
glamour.WithAutoStyle(),
glamour.WithWordWrap(width),
)
if err != nil {
return ""
}
out, err := renderer.Render(s)
if err != nil {
log.Println(err)
}
return out
}
return m.list.View()
}
Whole demo is recorded using VHS.
Conclusion
Bubble Tea is a powerful and elegant library for building text-based user interfaces in Go. Whether you are creating a simple command-line tool or a complex interactive application, Bubble Tea provides the tools you need to build high-quality TUIs with ease.
I had talk about Bubble Tea on Go meetup #15.
Give Bubble Tea a try, and you might find it to be the perfect ingredient for your next Go project!