mirror of
https://github.com/charmbracelet/gum
synced 2024-06-28 02:00:07 +02:00
feature(choose): Allow user to input additional value like in ComboBox
This commit is contained in:
parent
b0aba2261d
commit
67291a4f17
221
choose/choose.go
221
choose/choose.go
|
@ -11,28 +11,36 @@
|
|||
package choose
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/paginator"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/mattn/go-runewidth"
|
||||
)
|
||||
|
||||
type model struct {
|
||||
height int
|
||||
cursor string
|
||||
selectedPrefix string
|
||||
unselectedPrefix string
|
||||
cursorPrefix string
|
||||
items []item
|
||||
quitting bool
|
||||
index int
|
||||
limit int
|
||||
numSelected int
|
||||
paginator paginator.Model
|
||||
aborted bool
|
||||
type InputStyle int64
|
||||
|
||||
const (
|
||||
SELECT InputStyle = iota
|
||||
COMBOBOX
|
||||
INPUT
|
||||
)
|
||||
|
||||
type model struct {
|
||||
height int
|
||||
cursor string
|
||||
selectedPrefix string
|
||||
unselectedPrefix string
|
||||
cursorPrefix string
|
||||
items []item
|
||||
quitting bool
|
||||
index int
|
||||
limit int
|
||||
numSelected int
|
||||
allowAdditionalValue bool
|
||||
inputModel InputModels
|
||||
aborted bool
|
||||
// styles
|
||||
cursorStyle lipgloss.Style
|
||||
itemStyle lipgloss.Style
|
||||
|
@ -47,95 +55,120 @@ type item struct {
|
|||
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:
|
||||
return m, nil
|
||||
|
||||
case tea.KeyMsg:
|
||||
start, end := m.paginator.GetSliceBounds(len(m.items))
|
||||
switch keypress := msg.String(); keypress {
|
||||
case "down", "j", "ctrl+n":
|
||||
m.index++
|
||||
if m.index >= len(m.items) {
|
||||
m.index = 0
|
||||
m.paginator.Page = 0
|
||||
}
|
||||
if m.index >= end {
|
||||
m.paginator.NextPage()
|
||||
}
|
||||
case "up", "k", "ctrl+p":
|
||||
m.index--
|
||||
if m.index < 0 {
|
||||
if m.inputModel.inputState == SELECT {
|
||||
start, end := m.inputModel.paginator.GetSliceBounds(len(m.items))
|
||||
switch keypress := msg.String(); keypress {
|
||||
case "down", "j", "ctrl+n":
|
||||
m.index++
|
||||
if m.index >= len(m.items) {
|
||||
m.index = 0
|
||||
m.inputModel.paginator.Page = 0
|
||||
}
|
||||
if m.index >= end {
|
||||
m.inputModel.paginator.NextPage()
|
||||
}
|
||||
case "up", "k", "ctrl+p":
|
||||
m.index--
|
||||
if m.index < 0 {
|
||||
m.index = len(m.items) - 1
|
||||
m.inputModel.paginator.Page = m.inputModel.paginator.TotalPages - 1
|
||||
}
|
||||
if m.index < start {
|
||||
m.inputModel.paginator.PrevPage()
|
||||
}
|
||||
case "right", "l", "ctrl+f":
|
||||
m.index = clamp(m.index+m.height, 0, len(m.items)-1)
|
||||
m.inputModel.paginator.NextPage()
|
||||
case "left", "h", "ctrl+b":
|
||||
m.index = clamp(m.index-m.height, 0, len(m.items)-1)
|
||||
m.inputModel.paginator.PrevPage()
|
||||
case "G":
|
||||
m.index = len(m.items) - 1
|
||||
m.paginator.Page = m.paginator.TotalPages - 1
|
||||
}
|
||||
if m.index < start {
|
||||
m.paginator.PrevPage()
|
||||
}
|
||||
case "right", "l", "ctrl+f":
|
||||
m.index = clamp(m.index+m.height, 0, len(m.items)-1)
|
||||
m.paginator.NextPage()
|
||||
case "left", "h", "ctrl+b":
|
||||
m.index = clamp(m.index-m.height, 0, len(m.items)-1)
|
||||
m.paginator.PrevPage()
|
||||
case "G":
|
||||
m.index = len(m.items) - 1
|
||||
m.paginator.Page = m.paginator.TotalPages - 1
|
||||
case "g":
|
||||
m.index = 0
|
||||
m.paginator.Page = 0
|
||||
case "a":
|
||||
if m.limit <= 1 {
|
||||
break
|
||||
}
|
||||
for i := range m.items {
|
||||
if m.numSelected >= m.limit {
|
||||
break // do not exceed given limit
|
||||
m.inputModel.paginator.Page = m.inputModel.paginator.TotalPages - 1
|
||||
case "g":
|
||||
m.index = 0
|
||||
m.inputModel.paginator.Page = 0
|
||||
case "a":
|
||||
if m.limit <= 1 {
|
||||
break
|
||||
}
|
||||
if m.items[i].selected {
|
||||
continue
|
||||
for i := range m.items {
|
||||
if m.numSelected >= m.limit {
|
||||
break // do not exceed given limit
|
||||
}
|
||||
if m.items[i].selected {
|
||||
continue
|
||||
}
|
||||
m.items[i].selected = true
|
||||
m.numSelected++
|
||||
}
|
||||
case "A":
|
||||
if m.limit <= 1 {
|
||||
break
|
||||
}
|
||||
for i := range m.items {
|
||||
m.items[i].selected = false
|
||||
}
|
||||
m.numSelected = 0
|
||||
case "ctrl+c", "esc":
|
||||
m.aborted = true
|
||||
m.quitting = true
|
||||
return m, tea.Quit
|
||||
case " ", "tab", "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":
|
||||
if m.allowAdditionalValue && m.index == len(m.items)-1 {
|
||||
m.inputModel.inputState = INPUT
|
||||
m.inputModel.input.Focus()
|
||||
m.inputModel.input.CharLimit = 30
|
||||
return m, textinput.Blink
|
||||
} else {
|
||||
m.quitting = true
|
||||
// If the user hasn't selected any items in a multi-select.
|
||||
// Then we select the item that they have pressed enter on. If they
|
||||
// have selected items, then we simply return them.
|
||||
if m.numSelected < 1 {
|
||||
m.items[m.index].selected = true
|
||||
}
|
||||
return m, tea.Quit
|
||||
}
|
||||
m.items[i].selected = true
|
||||
m.numSelected++
|
||||
}
|
||||
case "A":
|
||||
if m.limit <= 1 {
|
||||
break
|
||||
}
|
||||
for i := range m.items {
|
||||
m.items[i].selected = false
|
||||
}
|
||||
m.numSelected = 0
|
||||
case "ctrl+c", "esc":
|
||||
m.aborted = true
|
||||
m.quitting = true
|
||||
return m, tea.Quit
|
||||
case " ", "tab", "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
|
||||
// If the user hasn't selected any items in a multi-select.
|
||||
// Then we select the item that they have pressed enter on. If they
|
||||
// have selected items, then we simply return them.
|
||||
if m.numSelected < 1 {
|
||||
m.items[m.index].selected = true
|
||||
} else if m.inputModel.inputState == INPUT {
|
||||
switch keypress := msg.String(); keypress {
|
||||
case "enter":
|
||||
value := m.inputModel.input.Value()
|
||||
if len(value) == 0 {
|
||||
m.inputModel.inputState = SELECT
|
||||
} else {
|
||||
m.items[m.index] = item{text: value, selected: true}
|
||||
m.inputModel.inputState = SELECT
|
||||
return m, tea.Quit
|
||||
}
|
||||
}
|
||||
return m, tea.Quit
|
||||
}
|
||||
}
|
||||
|
||||
var cmd tea.Cmd
|
||||
m.paginator, cmd = m.paginator.Update(msg)
|
||||
|
||||
cmd = m.inputModel.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
|
@ -144,9 +177,13 @@ func (m model) View() string {
|
|||
return ""
|
||||
}
|
||||
|
||||
if m.inputModel.inputState == INPUT {
|
||||
return m.inputModel.input.View()
|
||||
}
|
||||
|
||||
var s strings.Builder
|
||||
|
||||
start, end := m.paginator.GetSliceBounds(len(m.items))
|
||||
start, end := m.inputModel.paginator.GetSliceBounds(len(m.items))
|
||||
for i, item := range m.items[start:end] {
|
||||
if i == m.index%m.height {
|
||||
s.WriteString(m.cursorStyle.Render(m.cursor))
|
||||
|
@ -166,12 +203,12 @@ func (m model) View() string {
|
|||
}
|
||||
}
|
||||
|
||||
if m.paginator.TotalPages <= 1 {
|
||||
if m.inputModel.paginator.TotalPages <= 1 {
|
||||
return s.String()
|
||||
}
|
||||
|
||||
s.WriteString(strings.Repeat("\n", m.height-m.paginator.ItemsOnPage(len(m.items))+1))
|
||||
s.WriteString(" " + m.paginator.View())
|
||||
s.WriteString(strings.Repeat("\n", m.height-m.inputModel.paginator.ItemsOnPage(len(m.items))+1))
|
||||
s.WriteString(" " + m.inputModel.paginator.View())
|
||||
|
||||
return s.String()
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package choose
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
|
@ -77,6 +78,11 @@ func (o Options) Run() error {
|
|||
items[i] = item{text: option, selected: isSelected}
|
||||
}
|
||||
|
||||
// Adding an extra Field which can be selected in order to enter a new value
|
||||
if o.AllowInput {
|
||||
items = append(items, item{text: o.Prompt, selected: false})
|
||||
}
|
||||
|
||||
// Use the pagination model to display the current and total number of
|
||||
// pages.
|
||||
pager := paginator.New()
|
||||
|
@ -92,20 +98,24 @@ func (o Options) Run() error {
|
|||
pager.UseJKKeys = false
|
||||
pager.UsePgUpPgDownKeys = false
|
||||
|
||||
input := textinput.New()
|
||||
input.Placeholder = o.Placeholder
|
||||
|
||||
tm, err := tea.NewProgram(model{
|
||||
index: startingIndex,
|
||||
height: o.Height,
|
||||
cursor: o.Cursor,
|
||||
selectedPrefix: o.SelectedPrefix,
|
||||
unselectedPrefix: o.UnselectedPrefix,
|
||||
cursorPrefix: o.CursorPrefix,
|
||||
items: items,
|
||||
limit: o.Limit,
|
||||
paginator: pager,
|
||||
cursorStyle: o.CursorStyle.ToLipgloss(),
|
||||
itemStyle: o.ItemStyle.ToLipgloss(),
|
||||
selectedItemStyle: o.SelectedItemStyle.ToLipgloss(),
|
||||
numSelected: currentSelected,
|
||||
index: startingIndex,
|
||||
height: o.Height,
|
||||
cursor: o.Cursor,
|
||||
selectedPrefix: o.SelectedPrefix,
|
||||
unselectedPrefix: o.UnselectedPrefix,
|
||||
cursorPrefix: o.CursorPrefix,
|
||||
items: items,
|
||||
limit: o.Limit,
|
||||
inputModel: InputModels{paginator: pager, input: input, inputState: SELECT},
|
||||
cursorStyle: o.CursorStyle.ToLipgloss(),
|
||||
itemStyle: o.ItemStyle.ToLipgloss(),
|
||||
selectedItemStyle: o.SelectedItemStyle.ToLipgloss(),
|
||||
numSelected: currentSelected,
|
||||
allowAdditionalValue: o.AllowInput,
|
||||
}, tea.WithOutput(os.Stderr)).Run()
|
||||
|
||||
if err != nil {
|
||||
|
|
31
choose/inputmodels.go
Normal file
31
choose/inputmodels.go
Normal file
|
@ -0,0 +1,31 @@
|
|||
package choose
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/bubbles/paginator"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
type InputType int
|
||||
|
||||
const (
|
||||
Select InputType = iota
|
||||
Input
|
||||
)
|
||||
|
||||
type InputModels struct {
|
||||
inputState InputStyle
|
||||
paginator paginator.Model
|
||||
input textinput.Model
|
||||
}
|
||||
|
||||
func (m *InputModels) Update(msg tea.Msg) tea.Cmd {
|
||||
var cmd tea.Cmd
|
||||
switch m.inputState {
|
||||
case SELECT:
|
||||
m.paginator, cmd = m.paginator.Update(msg)
|
||||
case INPUT:
|
||||
m.input, cmd = m.input.Update(msg)
|
||||
}
|
||||
return cmd
|
||||
}
|
|
@ -8,6 +8,9 @@ type Options struct {
|
|||
|
||||
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"`
|
||||
AllowInput bool `help:"Allow the Input of additional value" group:"Selection"`
|
||||
Placeholder string `help:"Placeholder value asking for user Input of additional value" default:"Type something..." env:"GUM_INPUT_PLACEHOLDER"`
|
||||
Prompt string `help:"Prompt to display as extra List Item" default:"> " env:"GUM_INPUT_PROMPT"`
|
||||
Height int `help:"Height of the list" default:"10" env:"GUM_CHOOSE_HEIGHT"`
|
||||
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"`
|
||||
|
|
Loading…
Reference in a new issue