feat(choose): Ability to choose multiple choices --limit

This commit introduces the ability to choose multiple options from the list of choices in `gum choose` by adding a `--limit` flag.
This commit is contained in:
Maas Lalani 2022-07-13 11:45:52 -04:00
parent 5de4df66d2
commit 01404ef586
No known key found for this signature in database
GPG key ID: 5A6ED5CBF1A0A000
3 changed files with 121 additions and 82 deletions

View file

@ -12,57 +12,35 @@
package choose
import (
"fmt"
"io"
"strings"
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/mattn/go-runewidth"
)
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
}
height int
page int
cursor string
selectedPrefix string
unselectedPrefix string
cursorPrefix string
items []item
quitting bool
index int
limit int
numSelected int
type item string
func (i item) FilterValue() string { return "" }
type itemDelegate struct {
indicator string
indicatorStyle lipgloss.Style
// styles
cursorStyle 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))
type item struct {
text string
selected bool
}
func (m model) Init() tea.Cmd { return nil }
@ -70,43 +48,92 @@ 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 "down", "j", "ctrl+n":
m.index = (m.index + 1) % len(m.items)
m.page = m.index / m.height
case "up", "k", "ctrl+p":
m.index = (m.index - 1 + len(m.items)) % len(m.items)
m.page = m.index / m.height
case "right", "l", "ctrl+f":
if m.index+m.height < len(m.items) {
m.index += m.height
} else {
if m.page < len(m.items)/m.height {
m.index = len(m.items) - 1
}
}
m.page = m.index / m.height
case "left", "h", "ctrl+b":
if m.index-m.height >= 0 {
m.index -= m.height
}
m.page = m.index / m.height
case "ctrl+c":
m.quitting = true
return m, tea.Quit
case " ", "x":
if m.limit == 1 {
break // no op
}
if m.items[m.index].selected {
m.items[m.index].selected = false
m.numSelected--
} else if m.numSelected < m.limit {
m.items[m.index].selected = true
m.numSelected++
}
case "enter":
m.quitting = true
i, ok := m.list.SelectedItem().(item)
if ok {
m.choice = string(i)
// Select the item on which they've hit enter if it falls within
// the limit.
if m.numSelected < m.limit {
m.items[m.index].selected = true
}
return m, tea.Quit
}
}
var cmd tea.Cmd
m.list, cmd = m.list.Update(msg)
return m, cmd
return m, nil
}
func (m model) View() string {
if m.quitting {
return ""
}
return m.list.View()
var s strings.Builder
for i, item := range m.items[clamp(m.page*m.height, 0, len(m.items)):clamp((m.page+1)*m.height, 0, len(m.items))] {
if i == m.index%m.height {
s.WriteString(m.cursorStyle.Render(m.cursor))
} else {
s.WriteString(strings.Repeat(" ", runewidth.StringWidth(m.cursor)))
}
if item.selected {
s.WriteString(m.selectedItemStyle.Render(m.selectedPrefix + item.text))
} else if i == m.index%m.height {
s.WriteString(m.cursorStyle.Render(m.cursorPrefix + item.text))
} else {
s.WriteString(m.itemStyle.Render(m.unselectedPrefix + item.text))
}
s.WriteRune('\n')
}
return s.String()
}
func clamp(min, max, val int) int {
if val < min {
func clamp(x, min, max int) int {
if x < min {
return min
}
if val > max {
if x > max {
return max
}
return val
return x
}

View file

@ -5,10 +5,8 @@ import (
"os"
"strings"
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/gum/internal/stdin"
"github.com/mattn/go-runewidth"
)
// Run provides a shell script interface for choosing between different through
@ -16,34 +14,45 @@ import (
func (o Options) Run() error {
if len(o.Options) == 0 {
input, _ := stdin.Read()
o.Options = strings.Split(input, "\n")
o.Options = strings.Split(strings.TrimSpace(input), "\n")
}
items := []list.Item{}
var items []item
for _, option := range o.Options {
if option == "" {
continue
items = append(items, item{text: option, selected: false})
}
// We don't need to display prefixes if we are only picking one option.
// Simply displaying the cursor is enough.
if o.Limit == 1 {
o.SelectedPrefix = ""
o.UnselectedPrefix = ""
o.CursorPrefix = ""
}
m, err := tea.NewProgram(model{
height: o.Height,
cursor: o.Cursor,
selectedPrefix: o.SelectedPrefix,
unselectedPrefix: o.UnselectedPrefix,
cursorPrefix: o.CursorPrefix,
items: items,
limit: o.Limit,
cursorStyle: o.CursorStyle.ToLipgloss(),
itemStyle: o.ItemStyle.ToLipgloss(),
selectedItemStyle: o.SelectedItemStyle.ToLipgloss(),
}, tea.WithOutput(os.Stderr)).StartReturningModel()
var s strings.Builder
for _, item := range m.(model).items {
if item.selected {
s.WriteString(item.text)
s.WriteRune('\n')
}
items = append(items, item(option))
}
const defaultWidth = 20
fmt.Println(strings.TrimSuffix(s.String(), "\n"))
id := itemDelegate{
indicator: o.Indicator,
indicatorStyle: o.IndicatorStyle.ToLipgloss(),
selectedItemStyle: o.SelectedStyle.ToLipgloss(),
itemStyle: o.ItemStyle.ToLipgloss().MarginLeft(runewidth.StringWidth(o.Indicator)),
}
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()
fmt.Println(m.(model).choice)
return err
}

View file

@ -6,10 +6,13 @@ import "github.com/charmbracelet/gum/style"
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:"> "`
IndicatorStyle style.Styles `embed:"" prefix:"indicator." set:"defaultForeground=212" set:"name=indicator"`
ItemStyle style.Styles `embed:"" prefix:"item." hidden:"" set:"defaultForeground=255" set:"name=item"`
SelectedStyle style.Styles `embed:"" prefix:"selected." set:"defaultForeground=212" set:"name=selected item"`
Limit int `help:"Maximum number of options to pick" default:"1"`
Height int `help:"Height of the list" default:"10"`
Cursor string `help:"Prefix to show on item that corresponds to the cursor position" default:"> "`
CursorPrefix string `help:"Prefix to show on the cursor item (hidden if limit is 1)" default:"• "`
SelectedPrefix string `help:"Prefix to show on selected items (hidden if limit is 1)" default:"✕ "`
UnselectedPrefix string `help:"Prefix to show on selected items (hidden if limit is 1)" default:" "`
CursorStyle style.Styles `embed:"" prefix:"cursor." set:"defaultForeground=212" set:"name=indicator"`
ItemStyle style.Styles `embed:"" prefix:"item." hidden:"" set:"defaultForeground=255" set:"name=item"`
SelectedItemStyle style.Styles `embed:"" prefix:"selected." set:"defaultForeground=212" set:"name=selected item"`
}