Add list examples

This commit is contained in:
Christian Muehlhaeuser
2021-03-02 22:52:27 +01:00
parent 1b8495ed21
commit 27ba7e7aa1
7 changed files with 672 additions and 0 deletions

3
.gitignore vendored
View File

@@ -4,6 +4,9 @@
examples/fullscreen/fullscreen
examples/help/help
examples/http/http
examples/list-default/list-default
examples/list-fancy/list-fancy
examples/list-simple/list-simple
examples/mouse/mouse
examples/pager/pager
examples/simple/simple

View File

@@ -29,6 +29,7 @@ github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55k
github.com/fogleman/ease v0.0.0-20170301025033-8da417bf1776 h1:VRIbnDWRmAh5yBdz+J6yFMF5vso1It6vn+WmM/5l7MA=
github.com/fogleman/ease v0.0.0-20170301025033-8da417bf1776/go.mod h1:9wvnDu3YOfxzWM9Cst40msBF1C2UdQgDv962oTxSuMs=
github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f/go.mod h1:nOFQdrUlIlx6M6ODdSpBj1NVA+VgLC6kmw60mkw34H4=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
@@ -63,6 +64,7 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=

View File

@@ -0,0 +1,87 @@
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, nil
}
case tea.WindowSizeMsg:
top, right, bottom, left := docStyle.GetMargin()
m.list.SetSize(msg.Width-left-right, msg.Height-top-bottom)
}
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 Pis", 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: "Its 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: "Gaffers tape", desc: "Basically sticky fabric"},
item{title: "Terrycloth", desc: "In other words, towel fabric"},
}
m := model{list: list.NewModel(items, list.NewDefaultDelegate(), 0, 0)}
m.list.Title = "My Fave Things"
p := tea.NewProgram(m)
p.EnterAltScreen()
if err := p.Start(); err != nil {
fmt.Println("Error running program:", err)
os.Exit(1)
}
}

View File

@@ -0,0 +1,89 @@
package main
import (
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
)
func newItemDelegate(keys *delegateKeyMap) list.DefaultDelegate {
d := list.NewDefaultDelegate()
d.UpdateFunc = func(msg tea.Msg, m *list.Model) tea.Cmd {
var title string
if i, ok := m.SelectedItem().(item); ok {
title = i.Title()
} else {
return nil
}
switch msg := msg.(type) {
case tea.KeyMsg:
switch {
case key.Matches(msg, keys.choose):
return m.NewStatusMessage(statusMessageStyle("You chose " + title))
case key.Matches(msg, keys.remove):
index := m.Index()
m.RemoveItem(index)
if len(m.Items()) == 0 {
keys.remove.SetEnabled(false)
}
return m.NewStatusMessage(statusMessageStyle("Deleted " + title))
}
}
return nil
}
help := []key.Binding{keys.choose, keys.remove}
d.ShortHelpFunc = func() []key.Binding {
return help
}
d.FullHelpFunc = func() [][]key.Binding {
return [][]key.Binding{help}
}
return d
}
type delegateKeyMap struct {
choose key.Binding
remove key.Binding
}
// Additional short help entries. This satisfies the help.KeyMap interface and
// is entirely optional.
func (d delegateKeyMap) ShortHelp() []key.Binding {
return []key.Binding{
d.choose,
d.remove,
}
}
// Additional full help entries. This satisfies the help.KeyMap interface and
// is entirely optional.
func (d delegateKeyMap) FullHelp() [][]key.Binding {
return [][]key.Binding{
{
d.choose,
d.remove,
},
}
}
func newDelegateKeyMap() *delegateKeyMap {
return &delegateKeyMap{
choose: key.NewBinding(
key.WithKeys("enter"),
key.WithHelp("enter", "choose"),
),
remove: key.NewBinding(
key.WithKeys("x", "backspace"),
key.WithHelp("x", "delete"),
),
}
}

190
examples/list-fancy/main.go Normal file
View File

@@ -0,0 +1,190 @@
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.NewModel(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 tea.EnterAltScreen
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
switch msg := msg.(type) {
case tea.WindowSizeMsg:
topGap, rightGap, bottomGap, leftGap := appStyle.GetPadding()
m.list.SetSize(msg.Width-leftGap-rightGap, msg.Height-topGap-bottomGap)
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()).Start(); err != nil {
fmt.Println("Error running program:", err)
os.Exit(1)
}
}

View File

@@ -0,0 +1,164 @@
package main
import (
"math/rand"
"sync"
)
type randomItemGenerator struct {
titles []string
descs []string
titleIndex int
descIndex int
mtx *sync.Mutex
shuffle *sync.Once
}
func (r *randomItemGenerator) reset() {
r.mtx = &sync.Mutex{}
r.shuffle = &sync.Once{}
r.titles = []string{
"Artichoke",
"Baking Flour",
"Bananas",
"Barley",
"Bean Sprouts",
"Bitter Melon",
"Black Cod",
"Blood Orange",
"Brown Sugar",
"Cashew Apple",
"Cashews",
"Cat Food",
"Coconut Milk",
"Cucumber",
"Curry Paste",
"Currywurst",
"Dill",
"Dragonfruit",
"Dried Shrimp",
"Eggs",
"Fish Cake",
"Furikake",
"Garlic",
"Gherkin",
"Ginger",
"Granulated Sugar",
"Grapefruit",
"Green Onion",
"Hazelnuts",
"Heavy whipping cream",
"Honey Dew",
"Horseradish",
"Jicama",
"Kohlrabi",
"Leeks",
"Lentils",
"Licorice Root",
"Meyer Lemons",
"Milk",
"Molasses",
"Muesli",
"Nectarine",
"Niagamo Root",
"Nopal",
"Nutella",
"Oat Milk",
"Oatmeal",
"Olives",
"Papaya",
"Party Gherkin",
"Peppers",
"Persian Lemons",
"Pickle",
"Pineapple",
"Plantains",
"Pocky",
"Powdered Sugar",
"Quince",
"Radish",
"Ramps",
"Star Anise",
"Sweet Potato",
"Tamarind",
"Unsalted Butter",
"Watermelon",
"Weißwurst",
"Yams",
"Yeast",
"Yuzu",
"Snow Peas",
}
r.descs = []string{
"A little weird",
"Bold flavor",
"Cant get enough",
"Delectable",
"Expensive",
"Expired",
"Exquisite",
"Fresh",
"Gimme",
"In season",
"Kind of spicy",
"Looks fresh",
"Looks good to me",
"Maybe not",
"My favorite",
"Oh my",
"On sale",
"Organic",
"Questionable",
"Really fresh",
"Refreshing",
"Salty",
"Scrumptious",
"Delectable",
"Slightly sweet",
"Smells great",
"Tasty",
"Too ripe",
"At last",
"What?",
"Wow",
"Yum",
"Maybe",
"Sure, why not?",
}
r.shuffle.Do(func() {
shuf := func(x []string) {
rand.Shuffle(len(x), func(i, j int) { x[i], x[j] = x[j], x[i] })
}
shuf(r.titles)
shuf(r.descs)
})
}
func (r *randomItemGenerator) next() item {
if r.mtx == nil {
r.reset()
}
r.mtx.Lock()
defer r.mtx.Unlock()
i := item{
title: r.titles[r.titleIndex],
description: r.descs[r.descIndex],
}
r.titleIndex++
if r.titleIndex >= len(r.titles) {
r.titleIndex = 0
}
r.descIndex++
if r.descIndex >= len(r.descs) {
r.descIndex = 0
}
return i
}

View File

@@ -0,0 +1,137 @@
package main
import (
"fmt"
"io"
"os"
"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 string(i) }
type itemDelegate struct{}
func (d itemDelegate) Height() int { return 1 }
func (d itemDelegate) Spacing() int { return 0 }
func (d itemDelegate) Update(msg tea.Msg, m *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("> " + s)
}
}
fmt.Fprintf(w, fn(str))
}
type model struct {
list list.Model
items []item
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 "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
default:
if !m.list.SettingFilter() && (keypress == "q" || keypress == "esc") {
m.quitting = true
return m, tea.Quit
}
var cmd tea.Cmd
m.list, cmd = m.list.Update(msg)
return m, cmd
}
default:
return m, nil
}
}
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? Thats 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.NewModel(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).Start(); err != nil {
fmt.Println("Error running program:", err)
os.Exit(1)
}
}