mirror of
https://github.com/charmbracelet/gum
synced 2024-05-21 07:36:44 +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
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue