feat: gum choose, pick from a list of choices

gum choose allows the user to be prompted for a choice from a list of choices.

For example, let's ask the user to pick a card from a deck.
gum choose --height 15 {Ace,King,Queen,Jack,Ten,Nine,Eight,Seven,Six,Five,Four,Three,Two}" of "{Spades,Hearts,Clubs,Diamonds}
This commit is contained in:
Maas Lalani 2022-07-11 16:17:47 -04:00
parent dab3792d5f
commit 46ddc28ae5
No known key found for this signature in database
GPG key ID: 5A6ED5CBF1A0A000
6 changed files with 195 additions and 1 deletions

View file

@ -28,7 +28,7 @@ gum spin --title "Taking a nap..." --color 212 -- sleep 5
find . -type f | gum filter
```
The following example is running from a [single Bash script](./examples/demo.sh).
The following example is running from a [single bash script](./examples/demo.sh).
<img src="https://stuff.charm.sh/gum/gum.gif" width="900" alt="Shell running the Gum examples/demo.sh script">
@ -101,6 +101,17 @@ echo Cherry >> flavors.text
cat flavors.text | gum filter > selection.text
```
#### Choose
Ask your users to choose an option from a list of choices.
```bash
echo "Pick a card, any card..."
CARD=$(gum choose --height 15 {{A,K,Q,J},{10..2}}" "{♠,♥,♣,♦})
echo "Was your card the $CARD?"
```
#### Progress
Display a progress bar while loading. The following command will display a

101
choose/choose.go Normal file
View file

@ -0,0 +1,101 @@
package choose
import (
"fmt"
"io"
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
type model struct {
choice string
height int
indicator string
indicatorStyle lipgloss.Style
itemStyle lipgloss.Style
items []item
list list.Model
options []string
quitting bool
selectedItemStyle lipgloss.Style
}
type item string
func (i item) FilterValue() string { return "" }
type itemDelegate struct {
indicator string
indicatorStyle lipgloss.Style
itemStyle lipgloss.Style
selectedItemStyle lipgloss.Style
}
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("%s", i)
fn := d.itemStyle.Render
if index == m.Index() {
fn = func(s string) string {
return d.indicatorStyle.Render(d.indicator) + d.selectedItemStyle.Render(s)
}
}
fmt.Fprintf(w, fn(str))
}
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":
m.quitting = true
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.quitting {
return ""
}
return m.list.View()
}
func clamp(min, max, val int) int {
if val < min {
return min
}
if val > max {
return max
}
return val
}

47
choose/command.go Normal file
View file

@ -0,0 +1,47 @@
package choose
import (
"fmt"
"os"
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/mattn/go-runewidth"
)
// Run provides a shell script interface for choosing between different through
// options.
func (o Options) Run() {
items := []list.Item{}
for _, option := range o.Options {
if option == "" {
continue
}
items = append(items, item(option))
}
const defaultWidth = 20
id := itemDelegate{
indicator: o.Indicator,
indicatorStyle: lipgloss.NewStyle().Foreground(lipgloss.Color(o.IndicatorColor)),
itemStyle: lipgloss.NewStyle().Padding(0, runewidth.StringWidth(o.Indicator)).Foreground(lipgloss.Color(o.UnselectedColor)),
selectedItemStyle: lipgloss.NewStyle().Foreground(lipgloss.Color(o.SelectedColor)),
}
l := list.New(items, id, defaultWidth, o.Height)
l.SetShowTitle(false)
l.SetShowStatusBar(false)
l.SetFilteringEnabled(false)
l.SetShowHelp(false)
l.SetShowPagination(!o.HidePagination)
m, err := tea.NewProgram(model{list: l}, tea.WithOutput(os.Stderr)).StartReturningModel()
if err != nil {
fmt.Println("Error running program:", err)
os.Exit(1)
}
fmt.Println(m.(model).choice)
}

13
choose/options.go Normal file
View file

@ -0,0 +1,13 @@
package choose
// Options is the customization options for the choose command.
type Options struct {
Options []string `arg:"" optional:"" help:"Options to choose from."`
Height int `help:"Height of the list." default:"10"`
HidePagination bool `help:"Hide pagination." default:"false"`
Indicator string `help:"Prefix to show on selected item" default:"> "`
IndicatorColor string `help:"Indicator foreground color" default:"212"`
SelectedColor string `help:"Selected item foreground color" default:"212"`
UnselectedColor string `help:"Unselected item foreground color" default:""`
}

14
gum.go
View file

@ -1,6 +1,7 @@
package main
import (
"github.com/charmbracelet/gum/choose"
"github.com/charmbracelet/gum/filter"
"github.com/charmbracelet/gum/input"
"github.com/charmbracelet/gum/join"
@ -45,6 +46,19 @@ type Gum struct {
//
Filter filter.Options `cmd:"" help:"Filter options through fuzzy search."`
// Choose provides an interface to choose one option from a given list of
// options. The options can be provided as (new-line separated) stdin or a
// list of arguments.
//
// It is different from the filter command as it does not provide a fuzzy
// finding input, so it is best used for smaller lists of options.
//
// Let's pick from a list of gum flavors:
//
// $ gum choose "Strawberry" "Banana" "Cherry"
//
Choose choose.Options `cmd:"" help:"Choose from a list of options."`
// Spin provides a shell script interface for the spinner bubble.
// https://github.com/charmbracelet/bubbles/tree/master/spinner
//

View file

@ -1,6 +1,8 @@
package main
import (
"strings"
"github.com/alecthomas/kong"
"github.com/charmbracelet/gum/internal/stdin"
"github.com/charmbracelet/lipgloss"
@ -25,6 +27,12 @@ func main() {
gum.Write.Run()
case "filter":
gum.Filter.Run()
case "choose":
input, _ := stdin.Read()
gum.Choose.Options = strings.Split(input, "\n")
gum.Choose.Run()
case "choose <options>":
gum.Choose.Run()
case "spin <command>":
gum.Spin.Run()
case "progress":