Components
List
This is progress snippet using bubbletea and lipgloss
What is this?
A customizable, batteries-included component for browsing a set of items. Features pagination, fuzzy filtering, auto-generated help, an activity spinner, and status messages, all of which can be enabled and disabled as needed. Extrapolated from [Glow][glow].
Default list
package main
import (
"fmt"
"os"
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
var docStyle = lipgloss.NewStyle().Margin(1, 2)
type item struct {
title, desc string
}
func (i item) Title() string { return i.title }
func (i item) Description() string { return i.desc }
func (i item) FilterValue() string { return i.title }
type model struct {
list list.Model
}
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.KeyMsg:
if msg.String() == "ctrl+c" {
return m, tea.Quit
}
case tea.WindowSizeMsg:
h, v := docStyle.GetFrameSize()
m.list.SetSize(msg.Width-h, msg.Height-v)
}
var cmd tea.Cmd
m.list, cmd = m.list.Update(msg)
return m, cmd
}
func (m model) View() string {
return docStyle.Render(m.list.View())
}
func main() {
items := []list.Item{
item{title: "Raspberry Pi’s", desc: "I have ’em all over my house"},
item{title: "Nutella", desc: "It's good on toast"},
item{title: "Bitter melon", desc: "It cools you down"},
item{title: "Nice socks", desc: "And by that I mean socks without holes"},
item{title: "Eight hours of sleep", desc: "I had this once"},
item{title: "Cats", desc: "Usually"},
item{title: "Plantasia, the album", desc: "My plants love it too"},
item{title: "Pour over coffee", desc: "It takes forever to make though"},
item{title: "VR", desc: "Virtual reality...what is there to say?"},
item{title: "Noguchi Lamps", desc: "Such pleasing organic forms"},
item{title: "Linux", desc: "Pretty much the best OS"},
item{title: "Business school", desc: "Just kidding"},
item{title: "Pottery", desc: "Wet clay is a great feeling"},
item{title: "Shampoo", desc: "Nothing like clean hair"},
item{title: "Table tennis", desc: "It’s surprisingly exhausting"},
item{title: "Milk crates", desc: "Great for packing in your extra stuff"},
item{title: "Afternoon tea", desc: "Especially the tea sandwich part"},
item{title: "Stickers", desc: "The thicker the vinyl the better"},
item{title: "20° Weather", desc: "Celsius, not Fahrenheit"},
item{title: "Warm light", desc: "Like around 2700 Kelvin"},
item{title: "The vernal equinox", desc: "The autumnal equinox is pretty good too"},
item{title: "Gaffer’s tape", desc: "Basically sticky fabric"},
item{title: "Terrycloth", desc: "In other words, towel fabric"},
}
m := model{list: list.New(items, list.NewDefaultDelegate(), 0, 0)}
m.list.Title = "My Fave Things"
p := tea.NewProgram(m, tea.WithAltScreen())
if _, err := p.Run(); err != nil {
fmt.Println("Error running program:", err)
os.Exit(1)
}
}Simple list
package main
import (
"fmt"
"io"
"os"
"strings"
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
const listHeight = 14
var (
titleStyle = lipgloss.NewStyle().MarginLeft(2)
itemStyle = lipgloss.NewStyle().PaddingLeft(4)
selectedItemStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(lipgloss.Color("170"))
paginationStyle = list.DefaultStyles().PaginationStyle.PaddingLeft(4)
helpStyle = list.DefaultStyles().HelpStyle.PaddingLeft(4).PaddingBottom(1)
quitTextStyle = lipgloss.NewStyle().Margin(1, 0, 2, 4)
)
type item string
func (i item) FilterValue() string { return "" }
type itemDelegate struct{}
func (d itemDelegate) Height() int { return 1 }
func (d itemDelegate) Spacing() int { return 0 }
func (d itemDelegate) Update(_ tea.Msg, _ *list.Model) tea.Cmd { return nil }
func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
i, ok := listItem.(item)
if !ok {
return
}
str := fmt.Sprintf("%d. %s", index+1, i)
fn := itemStyle.Render
if index == m.Index() {
fn = func(s ...string) string {
return selectedItemStyle.Render("> " + strings.Join(s, " "))
}
}
fmt.Fprint(w, fn(str))
}
type model struct {
list list.Model
choice string
quitting 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.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.choice = string(i)
}
return m, tea.Quit
}
}
var cmd tea.Cmd
m.list, cmd = m.list.Update(msg)
return m, cmd
}
func (m model) View() string {
if m.choice != "" {
return quitTextStyle.Render(fmt.Sprintf("%s? Sounds good to me.", m.choice))
}
if m.quitting {
return quitTextStyle.Render("Not hungry? That’s cool.")
}
return "\n" + m.list.View()
}
func main() {
items := []list.Item{
item("Ramen"),
item("Tomato Soup"),
item("Hamburgers"),
item("Cheeseburgers"),
item("Currywurst"),
item("Okonomiyaki"),
item("Pasta"),
item("Fillet Mignon"),
item("Caviar"),
item("Just Wine"),
}
const defaultWidth = 20
l := list.New(items, itemDelegate{}, defaultWidth, listHeight)
l.Title = "What do you want for dinner?"
l.SetShowStatusBar(false)
l.SetFilteringEnabled(false)
l.Styles.Title = titleStyle
l.Styles.PaginationStyle = paginationStyle
l.Styles.HelpStyle = helpStyle
m := model{list: l}
if _, err := tea.NewProgram(m).Run(); err != nil {
fmt.Println("Error running program:", err)
os.Exit(1)
}
}Feature rich list
package main
import (
"fmt"
"math/rand"
"os"
"time"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
var (
appStyle = lipgloss.NewStyle().Padding(1, 2)
titleStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#FFFDF5")).
Background(lipgloss.Color("#25A065")).
Padding(0, 1)
statusMessageStyle = lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#04B575", Dark: "#04B575"}).
Render
)
type item struct {
title string
description string
}
func (i item) Title() string { return i.title }
func (i item) Description() string { return i.description }
func (i item) FilterValue() string { return i.title }
type listKeyMap struct {
toggleSpinner key.Binding
toggleTitleBar key.Binding
toggleStatusBar key.Binding
togglePagination key.Binding
toggleHelpMenu key.Binding
insertItem key.Binding
}
func newListKeyMap() *listKeyMap {
return &listKeyMap{
insertItem: key.NewBinding(
key.WithKeys("a"),
key.WithHelp("a", "add item"),
),
toggleSpinner: key.NewBinding(
key.WithKeys("s"),
key.WithHelp("s", "toggle spinner"),
),
toggleTitleBar: key.NewBinding(
key.WithKeys("T"),
key.WithHelp("T", "toggle title"),
),
toggleStatusBar: key.NewBinding(
key.WithKeys("S"),
key.WithHelp("S", "toggle status"),
),
togglePagination: key.NewBinding(
key.WithKeys("P"),
key.WithHelp("P", "toggle pagination"),
),
toggleHelpMenu: key.NewBinding(
key.WithKeys("H"),
key.WithHelp("H", "toggle help"),
),
}
}
type model struct {
list list.Model
itemGenerator *randomItemGenerator
keys *listKeyMap
delegateKeys *delegateKeyMap
}
func newModel() model {
var (
itemGenerator randomItemGenerator
delegateKeys = newDelegateKeyMap()
listKeys = newListKeyMap()
)
// Make initial list of items
const numItems = 24
items := make([]list.Item, numItems)
for i := 0; i < numItems; i++ {
items[i] = itemGenerator.next()
}
// Setup list
delegate := newItemDelegate(delegateKeys)
groceryList := list.New(items, delegate, 0, 0)
groceryList.Title = "Groceries"
groceryList.Styles.Title = titleStyle
groceryList.AdditionalFullHelpKeys = func() []key.Binding {
return []key.Binding{
listKeys.toggleSpinner,
listKeys.insertItem,
listKeys.toggleTitleBar,
listKeys.toggleStatusBar,
listKeys.togglePagination,
listKeys.toggleHelpMenu,
}
}
return model{
list: groceryList,
keys: listKeys,
delegateKeys: delegateKeys,
itemGenerator: &itemGenerator,
}
}
func (m model) Init() tea.Cmd {
return nil
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
switch msg := msg.(type) {
case tea.WindowSizeMsg:
h, v := appStyle.GetFrameSize()
m.list.SetSize(msg.Width-h, msg.Height-v)
case tea.KeyMsg:
// Don't match any of the keys below if we're actively filtering.
if m.list.FilterState() == list.Filtering {
break
}
switch {
case key.Matches(msg, m.keys.toggleSpinner):
cmd := m.list.ToggleSpinner()
return m, cmd
case key.Matches(msg, m.keys.toggleTitleBar):
v := !m.list.ShowTitle()
m.list.SetShowTitle(v)
m.list.SetShowFilter(v)
m.list.SetFilteringEnabled(v)
return m, nil
case key.Matches(msg, m.keys.toggleStatusBar):
m.list.SetShowStatusBar(!m.list.ShowStatusBar())
return m, nil
case key.Matches(msg, m.keys.togglePagination):
m.list.SetShowPagination(!m.list.ShowPagination())
return m, nil
case key.Matches(msg, m.keys.toggleHelpMenu):
m.list.SetShowHelp(!m.list.ShowHelp())
return m, nil
case key.Matches(msg, m.keys.insertItem):
m.delegateKeys.remove.SetEnabled(true)
newItem := m.itemGenerator.next()
insCmd := m.list.InsertItem(0, newItem)
statusCmd := m.list.NewStatusMessage(statusMessageStyle("Added " + newItem.Title()))
return m, tea.Batch(insCmd, statusCmd)
}
}
// This will also call our delegate's update function.
newListModel, cmd := m.list.Update(msg)
m.list = newListModel
cmds = append(cmds, cmd)
return m, tea.Batch(cmds...)
}
func (m model) View() string {
return appStyle.Render(m.list.View())
}
func main() {
rand.Seed(time.Now().UTC().UnixNano())
if _, err := tea.NewProgram(newModel(), tea.WithAltScreen()).Run(); err != nil {
fmt.Println("Error running program:", err)
os.Exit(1)
}
}How is this guide?