https://github.com/charmbracelet/bubbletea
Bubble Tea programs are comprised of a model that describes the application state and three simple methods on that model:
- Init, a function that returns an initial command for the application to run.
- Update, a function that handles incoming events and updates the model accordingly.
- View, a function that renders the UI based on the data in the model.
model
A models stores are application's state. Usually with a struct:
type model struct {
choices []string // items on the to-do list
cursor int // which to-do list item our cursor is pointing at
selected map[int]struct{} // which to-do items are selected
}
init
We then create a variable with our model's initial state:
var initialModel = model{
// Our to-do list is just a grocery list
choices: []string{"Buy carrots", "Buy celery", "Buy kohlrabi"},
// A map which indicates which choices are selected. We're using
// the map like a mathematical set. The keys refer to the indexes
// of the `choices` slice, above.
selected: make(map[int]struct{}),
}
func (m model) Init() tea.Cmd {
// Just return `nil`, which means "no I/O right now, please."
return nil
}
Our Init
method just returns an empty tea.Cmd
, but more complicated programs
could use this to perform initial I/O.
update
Update is called when "things happen" and will update the model accordingly.
Optionally it can return a tea.Cmd
trigger more events, such as to quit the
program.
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg: // is it a keypress?
switch msg.String() { // what key was pressed?
case "ctrl+c", "q":
return m, tea.Quit
case "up", "k":
if m.cursor > 0 {
m.cursor--
}
case "down", "j":
if m.cursor < len(m.choices)-1 {
m.cursor++
}
case "enter", " ":
_, ok := m.selected[m.cursor]
if ok {
delete(m.selected, m.cursor)
} else {
m.selected[m.cursor] = struct{}{}
}
}
}
return m, nil
}
view
This is where we can render out UI. We look at the model in it's current state
and use it to return a string
. That's it.
func (m model) View() string {
// the header
s := "What should we buy at the market?\n\n"
for i, choice := range m.choices {
cursor := " " // no cursor
if m.cursor == i {
cursor = ">" // cursor!
}
checked := " " // no selection
if _, ok := m.selected[i]; ok {
checked = "x" // selected!
}
// render the row
s += fmt.Sprintf("%s [%s] %s\n", cursor, checked, choice)
}
// the footer
s += "\nPress q to quit.\n"
// Send the UI for rendering
return s
}
extras
https://github.com/muesli/reflow
ANSI aware text reflow operations.
https://github.com/muesli/termenv
ANSI styling and detection.
https://github.com/charmbracelet/lipgloss
A higher level combination of the above two for making Bubble tea views.
https://github.com/charmbracelet/bubbles
UI components for Bubble Tea.
https://github.com/erikgeiser/promptkit
Prompt components for Bubble Tea
https://github.com/mattn/go-runewidth
Calculate correct length of strings, len() doesn't account for double width characters.
https://github.com/charmbracelet/glamour
Stylesheet-based markdown rendering for your CLI apps.