From 46ddc28ae5bb9f052b50aa21fed129e046bd7f12 Mon Sep 17 00:00:00 2001 From: Maas Lalani Date: Mon, 11 Jul 2022 16:17:47 -0400 Subject: [PATCH] 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} --- README.md | 13 +++++- choose/choose.go | 101 ++++++++++++++++++++++++++++++++++++++++++++++ choose/command.go | 47 +++++++++++++++++++++ choose/options.go | 13 ++++++ gum.go | 14 +++++++ main.go | 8 ++++ 6 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 choose/choose.go create mode 100644 choose/command.go create mode 100644 choose/options.go diff --git a/README.md b/README.md index 14f7e9e..92f6d78 100644 --- a/README.md +++ b/README.md @@ -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). 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 diff --git a/choose/choose.go b/choose/choose.go new file mode 100644 index 0000000..46f5b95 --- /dev/null +++ b/choose/choose.go @@ -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 +} diff --git a/choose/command.go b/choose/command.go new file mode 100644 index 0000000..dfd4fdc --- /dev/null +++ b/choose/command.go @@ -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) +} diff --git a/choose/options.go b/choose/options.go new file mode 100644 index 0000000..8011ebb --- /dev/null +++ b/choose/options.go @@ -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:""` +} diff --git a/gum.go b/gum.go index 669fec2..3d5abdd 100644 --- a/gum.go +++ b/gum.go @@ -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 // diff --git a/main.go b/main.go index e4306d2..8b7e01a 100644 --- a/main.go +++ b/main.go @@ -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 ": + gum.Choose.Run() case "spin ": gum.Spin.Run() case "progress":