Skip to content

Commit ce4b3c2

Browse files
committed
feat: allow entering non-default values in multi-select
1 parent dcf5153 commit ce4b3c2

File tree

3 files changed

+130
-18
lines changed

3 files changed

+130
-18
lines changed

cli/cliui/select.go

Lines changed: 94 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ func (m selectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
195195
if m.cursor > 0 {
196196
m.cursor--
197197
} else {
198-
m.cursor = len(options) - 1
198+
m.cursor = len(options)
199199
}
200200

201201
case tea.KeyDown:
@@ -300,9 +300,10 @@ func (m selectModel) filteredOptions() []string {
300300
}
301301

302302
type MultiSelectOptions struct {
303-
Message string
304-
Options []string
305-
Defaults []string
303+
Message string
304+
Options []string
305+
Defaults []string
306+
EnableCustomInput bool
306307
}
307308

308309
func MultiSelect(inv *serpent.Invocation, opts MultiSelectOptions) ([]string, error) {
@@ -328,9 +329,10 @@ func MultiSelect(inv *serpent.Invocation, opts MultiSelectOptions) ([]string, er
328329
}
329330

330331
initialModel := multiSelectModel{
331-
search: textinput.New(),
332-
options: options,
333-
message: opts.Message,
332+
search: textinput.New(),
333+
options: options,
334+
message: opts.Message,
335+
enableCustomInput: opts.EnableCustomInput,
334336
}
335337

336338
initialModel.search.Prompt = ""
@@ -370,12 +372,15 @@ type multiSelectOption struct {
370372
}
371373

372374
type multiSelectModel struct {
373-
search textinput.Model
374-
options []*multiSelectOption
375-
cursor int
376-
message string
377-
canceled bool
378-
selected bool
375+
search textinput.Model
376+
options []*multiSelectOption
377+
cursor int
378+
message string
379+
canceled bool
380+
selected bool
381+
isInputMode bool // New field to track if we're adding a custom option
382+
customInput string // New field to store custom input
383+
enableCustomInput bool // New field to control whether custom input is allowed
379384
}
380385

381386
func (multiSelectModel) Init() tea.Cmd {
@@ -386,6 +391,10 @@ func (multiSelectModel) Init() tea.Cmd {
386391
func (m multiSelectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
387392
var cmd tea.Cmd
388393

394+
if m.isInputMode {
395+
return m.handleCustomInputMode(msg)
396+
}
397+
389398
switch msg := msg.(type) {
390399
case terminateMsg:
391400
m.canceled = true
@@ -398,6 +407,11 @@ func (m multiSelectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
398407
return m, tea.Quit
399408

400409
case tea.KeyEnter:
410+
// Switch to custom input mode if we're on the "+ Add custom value:" option
411+
if m.enableCustomInput && m.cursor == len(m.filteredOptions()) {
412+
m.isInputMode = true
413+
return m, nil
414+
}
401415
if len(m.options) != 0 {
402416
m.selected = true
403417
return m, tea.Quit
@@ -414,15 +428,17 @@ func (m multiSelectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
414428

415429
case tea.KeyUp:
416430
options := m.filteredOptions()
431+
maxIndex := len(options)
417432
if m.cursor > 0 {
418433
m.cursor--
419434
} else {
420-
m.cursor = len(options) - 1
435+
m.cursor = maxIndex
421436
}
422437

423438
case tea.KeyDown:
424439
options := m.filteredOptions()
425-
if m.cursor < len(options)-1 {
440+
maxIndex := len(options)
441+
if m.cursor < maxIndex {
426442
m.cursor++
427443
} else {
428444
m.cursor = 0
@@ -457,6 +473,52 @@ func (m multiSelectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
457473
return m, cmd
458474
}
459475

476+
// handleCustomInputMode manages keyboard interactions when in custom input mode
477+
func (m *multiSelectModel) handleCustomInputMode(msg tea.Msg) (tea.Model, tea.Cmd) {
478+
keyMsg, ok := msg.(tea.KeyMsg)
479+
if !ok {
480+
return m, nil
481+
}
482+
483+
switch keyMsg.Type {
484+
case tea.KeyEnter:
485+
return m.handleCustomInputSubmission()
486+
487+
case tea.KeyCtrlC:
488+
m.canceled = true
489+
return m, tea.Quit
490+
491+
case tea.KeyBackspace:
492+
return m.handleCustomInputBackspace()
493+
494+
default:
495+
m.customInput += keyMsg.String()
496+
return m, nil
497+
}
498+
}
499+
500+
// handleCustomInputSubmission processes the submission of custom input
501+
func (m *multiSelectModel) handleCustomInputSubmission() (tea.Model, tea.Cmd) {
502+
if m.customInput != "" {
503+
m.options = append(m.options, &multiSelectOption{
504+
option: m.customInput,
505+
chosen: true,
506+
})
507+
}
508+
// Reset input state regardless of whether input was empty
509+
m.customInput = ""
510+
m.isInputMode = false
511+
return m, nil
512+
}
513+
514+
// handleCustomInputBackspace handles backspace in custom input mode
515+
func (m *multiSelectModel) handleCustomInputBackspace() (tea.Model, tea.Cmd) {
516+
if len(m.customInput) > 0 {
517+
m.customInput = m.customInput[:len(m.customInput)-1]
518+
}
519+
return m, nil
520+
}
521+
460522
func (m multiSelectModel) View() string {
461523
var s strings.Builder
462524

@@ -469,13 +531,19 @@ func (m multiSelectModel) View() string {
469531
return s.String()
470532
}
471533

534+
if m.isInputMode {
535+
_, _ = s.WriteString(fmt.Sprintf("%s\nEnter custom value: %s\n", msg, m.customInput))
536+
return s.String()
537+
}
538+
472539
_, _ = s.WriteString(fmt.Sprintf(
473540
"%s %s[Use arrows to move, space to select, <right> to all, <left> to none, type to filter]\n",
474541
msg,
475542
m.search.View(),
476543
))
477544

478-
for i, option := range m.filteredOptions() {
545+
options := m.filteredOptions()
546+
for i, option := range options {
479547
cursor := " "
480548
chosen := "[ ]"
481549
o := option.option
@@ -498,6 +566,16 @@ func (m multiSelectModel) View() string {
498566
))
499567
}
500568

569+
if m.enableCustomInput {
570+
// Add the "+ Add custom value" option at the bottom
571+
cursor := " "
572+
text := " + Add custom value"
573+
if m.cursor == len(options) {
574+
cursor = pretty.Sprint(DefaultStyles.Keyword, "> ")
575+
text = pretty.Sprint(DefaultStyles.Keyword, text)
576+
}
577+
_, _ = s.WriteString(fmt.Sprintf("%s%s\n", cursor, text))
578+
}
501579
return s.String()
502580
}
503581

cli/cliui/select_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,39 @@ func TestMultiSelect(t *testing.T) {
101101
}()
102102
require.Equal(t, items, <-msgChan)
103103
})
104+
105+
t.Run("MultiSelectWithCustomInput", func(t *testing.T) {
106+
t.Parallel()
107+
items := []string{"Code", "Chairs", "Whale", "Diamond", "Carrot"}
108+
ptty := ptytest.New(t)
109+
msgChan := make(chan []string)
110+
go func() {
111+
resp, err := newMultiSelectWithCustomInput(ptty, items)
112+
assert.NoError(t, err)
113+
msgChan <- resp
114+
}()
115+
require.Equal(t, items, <-msgChan)
116+
})
117+
}
118+
119+
func newMultiSelectWithCustomInput(ptty *ptytest.PTY, items []string) ([]string, error) {
120+
var values []string
121+
cmd := &serpent.Command{
122+
Handler: func(inv *serpent.Invocation) error {
123+
selectedItems, err := cliui.MultiSelect(inv, cliui.MultiSelectOptions{
124+
Options: items,
125+
Defaults: items,
126+
EnableCustomInput: true,
127+
})
128+
if err == nil {
129+
values = selectedItems
130+
}
131+
return err
132+
},
133+
}
134+
inv := cmd.Invoke()
135+
ptty.Attach(inv)
136+
return values, inv.Run()
104137
}
105138

106139
func newMultiSelect(ptty *ptytest.PTY, items []string) ([]string, error) {

cli/prompts.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,9 +156,10 @@ func (RootCmd) promptExample() *serpent.Command {
156156
multiSelectValues, multiSelectError = cliui.MultiSelect(inv, cliui.MultiSelectOptions{
157157
Message: "Select some things:",
158158
Options: []string{
159-
"Code", "Chair", "Whale", "Diamond", "Carrot",
159+
"Code", "Chairs", "Whale", "Diamond", "Carrot",
160160
},
161-
Defaults: []string{"Code"},
161+
Defaults: []string{"Code"},
162+
EnableCustomInput: true,
162163
})
163164
}
164165
_, _ = fmt.Fprintf(inv.Stdout, "%q are nice choices.\n", strings.Join(multiSelectValues, ", "))

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy