mirror of
https://github.com/charmbracelet/gum.git
synced 2024-10-26 15:20:05 +03:00
feat(filter): Multi-select (#130)
* feat(filter): added multiple selections * docs(filter): corrected comments about multiSelection print * feat(filter): fix proposed changes on naming variables and map type * feat(filter): actually fix map types * feat(filter): use `○` / `◉` as unselected / selected multi-select options Co-authored-by: Maas Lalani <maas@lalani.dev>
This commit is contained in:
parent
81602545a6
commit
c9fe558a44
@ -12,7 +12,7 @@ type Options struct {
|
||||
Cursor string `help:"Prefix to show on item that corresponds to the cursor position" default:"> " env:"GUM_CHOOSE_CURSOR"`
|
||||
CursorPrefix string `help:"Prefix to show on the cursor item (hidden if limit is 1)" default:"[•] " env:"GUM_CHOOSE_CURSOR_PREFIX"`
|
||||
SelectedPrefix string `help:"Prefix to show on selected items (hidden if limit is 1)" default:"[✕] " env:"GUM_CHOOSE_SELECTED_PREFIX"`
|
||||
UnselectedPrefix string `help:"Prefix to show on selected items (hidden if limit is 1)" default:"[ ] " env:"GUM_CHOOSE_UNSELECTED_PREFIX"`
|
||||
UnselectedPrefix string `help:"Prefix to show on unselected items (hidden if limit is 1)" default:"[ ] " env:"GUM_CHOOSE_UNSELECTED_PREFIX"`
|
||||
CursorStyle style.Styles `embed:"" prefix:"cursor." set:"defaultForeground=212" envprefix:"GUM_CHOOSE_CURSOR_"`
|
||||
ItemStyle style.Styles `embed:"" prefix:"item." hidden:"" envprefix:"GUM_CHOOSE_ITEM_"`
|
||||
SelectedItemStyle style.Styles `embed:"" prefix:"selected." set:"defaultForeground=212" envprefix:"GUM_CHOOSE_SELECTED_"`
|
||||
|
@ -58,16 +58,26 @@ func (o Options) Run() error {
|
||||
matches = matchAll(choices)
|
||||
}
|
||||
|
||||
if o.NoLimit {
|
||||
o.Limit = len(choices)
|
||||
}
|
||||
|
||||
p := tea.NewProgram(model{
|
||||
choices: choices,
|
||||
indicator: o.Indicator,
|
||||
matches: matches,
|
||||
textinput: i,
|
||||
viewport: &v,
|
||||
indicatorStyle: o.IndicatorStyle.ToLipgloss(),
|
||||
matchStyle: o.MatchStyle.ToLipgloss(),
|
||||
textStyle: o.TextStyle.ToLipgloss(),
|
||||
height: o.Height,
|
||||
choices: choices,
|
||||
indicator: o.Indicator,
|
||||
matches: matches,
|
||||
textinput: i,
|
||||
viewport: &v,
|
||||
indicatorStyle: o.IndicatorStyle.ToLipgloss(),
|
||||
selectedPrefixStyle: o.SelectedPrefixStyle.ToLipgloss(),
|
||||
selectedPrefix: o.SelectedPrefix,
|
||||
unselectedPrefixStyle: o.UnselectedPrefixStyle.ToLipgloss(),
|
||||
unselectedPrefix: o.UnselectedPrefix,
|
||||
matchStyle: o.MatchStyle.ToLipgloss(),
|
||||
textStyle: o.TextStyle.ToLipgloss(),
|
||||
height: o.Height,
|
||||
selected: make(map[string]struct{}),
|
||||
limit: o.Limit,
|
||||
}, options...)
|
||||
|
||||
tm, err := p.StartReturningModel()
|
||||
@ -79,8 +89,16 @@ func (o Options) Run() error {
|
||||
if m.aborted {
|
||||
return exit.ErrAborted
|
||||
}
|
||||
if len(m.matches) > m.selected && m.selected >= 0 {
|
||||
fmt.Println(m.matches[m.selected].Str)
|
||||
|
||||
// allSelections contains values only if limit is greater
|
||||
// than 1 or if flag --no-limit is passed, hence there is
|
||||
// no need to further checks
|
||||
if len(m.selected) > 0 {
|
||||
for k := range m.selected {
|
||||
fmt.Println(k)
|
||||
}
|
||||
} else if len(m.matches) > m.cursor && m.cursor >= 0 {
|
||||
fmt.Println(m.matches[m.cursor].Str)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -22,18 +22,25 @@ import (
|
||||
)
|
||||
|
||||
type model struct {
|
||||
textinput textinput.Model
|
||||
viewport *viewport.Model
|
||||
choices []string
|
||||
matches []fuzzy.Match
|
||||
selected int
|
||||
indicator string
|
||||
height int
|
||||
aborted bool
|
||||
quitting bool
|
||||
matchStyle lipgloss.Style
|
||||
textStyle lipgloss.Style
|
||||
indicatorStyle lipgloss.Style
|
||||
textinput textinput.Model
|
||||
viewport *viewport.Model
|
||||
choices []string
|
||||
matches []fuzzy.Match
|
||||
cursor int
|
||||
selected map[string]struct{}
|
||||
limit int
|
||||
numSelected int
|
||||
indicator string
|
||||
selectedPrefix string
|
||||
unselectedPrefix string
|
||||
height int
|
||||
aborted bool
|
||||
quitting bool
|
||||
matchStyle lipgloss.Style
|
||||
textStyle lipgloss.Style
|
||||
indicatorStyle lipgloss.Style
|
||||
selectedPrefixStyle lipgloss.Style
|
||||
unselectedPrefixStyle lipgloss.Style
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd { return nil }
|
||||
@ -49,10 +56,19 @@ func (m model) View() string {
|
||||
for i, match := range m.matches {
|
||||
// If this is the current selected index, we add a small indicator to
|
||||
// represent it. Otherwise, simply pad the string.
|
||||
if i == m.selected {
|
||||
s.WriteString(m.indicatorStyle.Render(m.indicator) + " ")
|
||||
if i == m.cursor {
|
||||
s.WriteString(m.indicatorStyle.Render(m.indicator))
|
||||
} else {
|
||||
s.WriteString(strings.Repeat(" ", runewidth.StringWidth(m.indicator)) + " ")
|
||||
s.WriteString(strings.Repeat(" ", runewidth.StringWidth(m.indicator)))
|
||||
}
|
||||
|
||||
// If there are multiple selections mark them, otherwise leave an empty space
|
||||
if _, ok := m.selected[match.Str]; ok {
|
||||
s.WriteString(m.selectedPrefixStyle.Render(m.selectedPrefix))
|
||||
} else if m.limit > 1 {
|
||||
s.WriteString(m.unselectedPrefixStyle.Render(m.unselectedPrefix))
|
||||
} else {
|
||||
s.WriteString(" ")
|
||||
}
|
||||
|
||||
// For this match, there are a certain number of characters that have
|
||||
@ -102,14 +118,33 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.quitting = true
|
||||
return m, tea.Quit
|
||||
case "ctrl+n", "ctrl+j", "down":
|
||||
m.selected = clamp(0, len(m.matches)-1, m.selected+1)
|
||||
if m.selected >= m.viewport.YOffset+m.viewport.Height {
|
||||
m.cursor = clamp(0, len(m.matches)-1, m.cursor+1)
|
||||
if m.cursor >= m.viewport.YOffset+m.viewport.Height {
|
||||
m.viewport.LineDown(1)
|
||||
}
|
||||
case "ctrl+p", "ctrl+k", "up":
|
||||
m.selected = clamp(0, len(m.matches)-1, m.selected-1)
|
||||
if m.selected < m.viewport.YOffset {
|
||||
m.viewport.SetYOffset(m.selected)
|
||||
m.cursor = clamp(0, len(m.matches)-1, m.cursor-1)
|
||||
if m.cursor < m.viewport.YOffset {
|
||||
m.viewport.SetYOffset(m.cursor)
|
||||
}
|
||||
case "tab":
|
||||
if m.limit == 1 {
|
||||
break // no op
|
||||
}
|
||||
|
||||
// Tab is used to toggle selection of current item in the list
|
||||
if _, ok := m.selected[m.matches[m.cursor].Str]; ok {
|
||||
delete(m.selected, m.matches[m.cursor].Str)
|
||||
m.numSelected--
|
||||
} else if m.numSelected < m.limit {
|
||||
m.selected[m.matches[m.cursor].Str] = struct{}{}
|
||||
m.numSelected++
|
||||
}
|
||||
|
||||
// Go down by one line
|
||||
m.cursor = clamp(0, len(m.matches)-1, m.cursor+1)
|
||||
if m.cursor >= m.viewport.YOffset+m.viewport.Height {
|
||||
m.viewport.LineDown(1)
|
||||
}
|
||||
default:
|
||||
m.textinput, cmd = m.textinput.Update(msg)
|
||||
@ -130,7 +165,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
||||
// It's possible that filtering items have caused fewer matches. So, ensure
|
||||
// that the selected index is within the bounds of the number of matches.
|
||||
m.selected = clamp(0, len(m.matches)-1, m.selected)
|
||||
m.cursor = clamp(0, len(m.matches)-1, m.cursor)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
|
@ -4,14 +4,20 @@ import "github.com/charmbracelet/gum/style"
|
||||
|
||||
// Options is the customization options for the filter command.
|
||||
type Options struct {
|
||||
Indicator string `help:"Character for selection" default:"•" env:"GUM_FILTER_INDICATOR"`
|
||||
IndicatorStyle style.Styles `embed:"" prefix:"indicator." set:"defaultForeground=212" envprefix:"GUM_FILTER_INDICATOR_"`
|
||||
TextStyle style.Styles `embed:"" prefix:"text." envprefix:"GUM_FILTER_TEXT_"`
|
||||
MatchStyle style.Styles `embed:"" prefix:"match." set:"defaultForeground=212" envprefix:"GUM_FILTER_MATCH_"`
|
||||
Placeholder string `help:"Placeholder value" default:"Filter..." env:"GUM_FILTER_PLACEHOLDER"`
|
||||
Prompt string `help:"Prompt to display" default:"> " env:"GUM_FILTER_PROMPT"`
|
||||
PromptStyle style.Styles `embed:"" prefix:"prompt." set:"defaultForeground=240" envprefix:"GUM_FILTER_PROMPT_"`
|
||||
Width int `help:"Input width" default:"20" env:"GUM_FILTER_WIDTH"`
|
||||
Height int `help:"Input height" default:"0" env:"GUM_FILTER_HEIGHT"`
|
||||
Value string `help:"Initial filter value" default:"" env:"GUM_FILTER_VALUE"`
|
||||
Indicator string `help:"Character for selection" default:"•" env:"GUM_FILTER_INDICATOR"`
|
||||
IndicatorStyle style.Styles `embed:"" prefix:"indicator." set:"defaultForeground=212" envprefix:"GUM_FILTER_INDICATOR_"`
|
||||
Limit int `help:"Maximum number of options to pick" default:"1" group:"Selection"`
|
||||
NoLimit bool `help:"Pick unlimited number of options (ignores limit)" group:"Selection"`
|
||||
SelectedPrefix string `help:"Character to indicate selected items (hidden if limit is 1)" default:" ◉ " env:"GUM_FILTER_SELECTED_PREFIX"`
|
||||
SelectedPrefixStyle style.Styles `embed:"" prefix:"selected-indicator." set:"defaultForeground=212" envprefix:"GUM_FILTER_SELECTED_PREFIX_"`
|
||||
UnselectedPrefix string `help:"Character to indicate unselected items (hidden if limit is 1)" default:" ○ " env:"GUM_FILTER_UNSELECTED_PREFIX"`
|
||||
UnselectedPrefixStyle style.Styles `embed:"" prefix:"unselected-prefix." set:"defaultForeground=240" envprefix:"GUM_FILTER_UNSELECTED_PREFIX_"`
|
||||
TextStyle style.Styles `embed:"" prefix:"text." envprefix:"GUM_FILTER_TEXT_"`
|
||||
MatchStyle style.Styles `embed:"" prefix:"match." set:"defaultForeground=212" envprefix:"GUM_FILTER_MATCH_"`
|
||||
Placeholder string `help:"Placeholder value" default:"Filter..." env:"GUM_FILTER_PLACEHOLDER"`
|
||||
Prompt string `help:"Prompt to display" default:"> " env:"GUM_FILTER_PROMPT"`
|
||||
PromptStyle style.Styles `embed:"" prefix:"prompt." set:"defaultForeground=240" envprefix:"GUM_FILTER_PROMPT_"`
|
||||
Width int `help:"Input width" default:"20" env:"GUM_FILTER_WIDTH"`
|
||||
Height int `help:"Input height" default:"0" env:"GUM_FILTER_HEIGHT"`
|
||||
Value string `help:"Initial filter value" default:"" env:"GUM_FILTER_VALUE"`
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user