mirror of
https://github.com/charmbracelet/gum
synced 2024-06-03 06:12:19 +02:00
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:
parent
5de4df66d2
commit
01404ef586
131
choose/choose.go
131
choose/choose.go
|
@ -12,57 +12,35 @@
|
||||||
package choose
|
package choose
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"strings"
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/charmbracelet/bubbles/list"
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
"github.com/mattn/go-runewidth"
|
||||||
)
|
)
|
||||||
|
|
||||||
type model struct {
|
type model struct {
|
||||||
choice string
|
height int
|
||||||
height int
|
page int
|
||||||
indicator string
|
cursor string
|
||||||
indicatorStyle lipgloss.Style
|
selectedPrefix string
|
||||||
itemStyle lipgloss.Style
|
unselectedPrefix string
|
||||||
items []item
|
cursorPrefix string
|
||||||
list list.Model
|
items []item
|
||||||
options []string
|
quitting bool
|
||||||
quitting bool
|
index int
|
||||||
selectedItemStyle lipgloss.Style
|
limit int
|
||||||
}
|
numSelected int
|
||||||
|
|
||||||
type item string
|
// styles
|
||||||
|
cursorStyle lipgloss.Style
|
||||||
func (i item) FilterValue() string { return "" }
|
|
||||||
|
|
||||||
type itemDelegate struct {
|
|
||||||
indicator string
|
|
||||||
indicatorStyle lipgloss.Style
|
|
||||||
itemStyle lipgloss.Style
|
itemStyle lipgloss.Style
|
||||||
selectedItemStyle lipgloss.Style
|
selectedItemStyle lipgloss.Style
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d itemDelegate) Height() int { return 1 }
|
type item struct {
|
||||||
func (d itemDelegate) Spacing() int { return 0 }
|
text string
|
||||||
func (d itemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { return nil }
|
selected bool
|
||||||
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) 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) {
|
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case tea.WindowSizeMsg:
|
case tea.WindowSizeMsg:
|
||||||
m.list.SetWidth(msg.Width)
|
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
switch keypress := msg.String(); keypress {
|
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":
|
case "ctrl+c":
|
||||||
m.quitting = true
|
m.quitting = true
|
||||||
return m, tea.Quit
|
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":
|
case "enter":
|
||||||
m.quitting = true
|
m.quitting = true
|
||||||
i, ok := m.list.SelectedItem().(item)
|
// Select the item on which they've hit enter if it falls within
|
||||||
if ok {
|
// the limit.
|
||||||
m.choice = string(i)
|
if m.numSelected < m.limit {
|
||||||
|
m.items[m.index].selected = true
|
||||||
}
|
}
|
||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var cmd tea.Cmd
|
return m, nil
|
||||||
m.list, cmd = m.list.Update(msg)
|
|
||||||
return m, cmd
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m model) View() string {
|
func (m model) View() string {
|
||||||
if m.quitting {
|
if m.quitting {
|
||||||
return ""
|
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 {
|
func clamp(x, min, max int) int {
|
||||||
if val < min {
|
if x < min {
|
||||||
return min
|
return min
|
||||||
}
|
}
|
||||||
if val > max {
|
if x > max {
|
||||||
return max
|
return max
|
||||||
}
|
}
|
||||||
return val
|
return x
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,10 +5,8 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/charmbracelet/bubbles/list"
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/charmbracelet/gum/internal/stdin"
|
"github.com/charmbracelet/gum/internal/stdin"
|
||||||
"github.com/mattn/go-runewidth"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Run provides a shell script interface for choosing between different through
|
// Run provides a shell script interface for choosing between different through
|
||||||
|
@ -16,34 +14,45 @@ import (
|
||||||
func (o Options) Run() error {
|
func (o Options) Run() error {
|
||||||
if len(o.Options) == 0 {
|
if len(o.Options) == 0 {
|
||||||
input, _ := stdin.Read()
|
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 {
|
for _, option := range o.Options {
|
||||||
if option == "" {
|
items = append(items, item{text: option, selected: false})
|
||||||
continue
|
}
|
||||||
|
|
||||||
|
// 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
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,10 +6,13 @@ import "github.com/charmbracelet/gum/style"
|
||||||
type Options struct {
|
type Options struct {
|
||||||
Options []string `arg:"" optional:"" help:"Options to choose from."`
|
Options []string `arg:"" optional:"" help:"Options to choose from."`
|
||||||
|
|
||||||
Height int `help:"Height of the list" default:"10"`
|
Limit int `help:"Maximum number of options to pick" default:"1"`
|
||||||
HidePagination bool `help:"Hide pagination" default:"false"`
|
Height int `help:"Height of the list" default:"10"`
|
||||||
Indicator string `help:"Prefix to show on selected item" default:"> "`
|
Cursor string `help:"Prefix to show on item that corresponds to the cursor position" default:"> "`
|
||||||
IndicatorStyle style.Styles `embed:"" prefix:"indicator." set:"defaultForeground=212" set:"name=indicator"`
|
CursorPrefix string `help:"Prefix to show on the cursor item (hidden if limit is 1)" default:"• "`
|
||||||
ItemStyle style.Styles `embed:"" prefix:"item." hidden:"" set:"defaultForeground=255" set:"name=item"`
|
SelectedPrefix string `help:"Prefix to show on selected items (hidden if limit is 1)" default:"✕ "`
|
||||||
SelectedStyle style.Styles `embed:"" prefix:"selected." set:"defaultForeground=212" set:"name=selected item"`
|
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"`
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue