feature(choose): Allow user to input additional value like in ComboBox

This commit is contained in:
Dieter Eickstaedt 2022-11-09 10:05:34 +01:00
parent e38cfdaa10
commit 51a455a26e
4 changed files with 186 additions and 105 deletions

View file

@ -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()
}

View file

@ -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
View 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
}

View file

@ -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"`