Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitce4b3c2

Browse files
committed
feat: allow entering non-default values in multi-select
1 parentdcf5153 commitce4b3c2

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
ifm.cursor>0 {
196196
m.cursor--
197197
}else {
198-
m.cursor=len(options)-1
198+
m.cursor=len(options)
199199
}
200200

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

302302
typeMultiSelectOptionsstruct {
303-
Messagestring
304-
Options []string
305-
Defaults []string
303+
Messagestring
304+
Options []string
305+
Defaults []string
306+
EnableCustomInputbool
306307
}
307308

308309
funcMultiSelect(inv*serpent.Invocation,optsMultiSelectOptions) ([]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
typemultiSelectModelstruct {
373-
search textinput.Model
374-
options []*multiSelectOption
375-
cursorint
376-
messagestring
377-
canceledbool
378-
selectedbool
375+
search textinput.Model
376+
options []*multiSelectOption
377+
cursorint
378+
messagestring
379+
canceledbool
380+
selectedbool
381+
isInputModebool// New field to track if we're adding a custom option
382+
customInputstring// New field to store custom input
383+
enableCustomInputbool// 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 (mmultiSelectModel)Update(msg tea.Msg) (tea.Model, tea.Cmd) {
387392
varcmd tea.Cmd
388393

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

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

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

423438
casetea.KeyDown:
424439
options:=m.filteredOptions()
425-
ifm.cursor<len(options)-1 {
440+
maxIndex:=len(options)
441+
ifm.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
returnm,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+
returnm,nil
481+
}
482+
483+
switchkeyMsg.Type {
484+
casetea.KeyEnter:
485+
returnm.handleCustomInputSubmission()
486+
487+
casetea.KeyCtrlC:
488+
m.canceled=true
489+
returnm,tea.Quit
490+
491+
casetea.KeyBackspace:
492+
returnm.handleCustomInputBackspace()
493+
494+
default:
495+
m.customInput+=keyMsg.String()
496+
returnm,nil
497+
}
498+
}
499+
500+
// handleCustomInputSubmission processes the submission of custom input
501+
func (m*multiSelectModel)handleCustomInputSubmission() (tea.Model, tea.Cmd) {
502+
ifm.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+
returnm,nil
512+
}
513+
514+
// handleCustomInputBackspace handles backspace in custom input mode
515+
func (m*multiSelectModel)handleCustomInputBackspace() (tea.Model, tea.Cmd) {
516+
iflen(m.customInput)>0 {
517+
m.customInput=m.customInput[:len(m.customInput)-1]
518+
}
519+
returnm,nil
520+
}
521+
460522
func (mmultiSelectModel)View()string {
461523
vars strings.Builder
462524

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

534+
ifm.isInputMode {
535+
_,_=s.WriteString(fmt.Sprintf("%s\nEnter custom value: %s\n",msg,m.customInput))
536+
returns.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-
fori,option:=rangem.filteredOptions() {
545+
options:=m.filteredOptions()
546+
fori,option:=rangeoptions {
479547
cursor:=" "
480548
chosen:="[ ]"
481549
o:=option.option
@@ -498,6 +566,16 @@ func (m multiSelectModel) View() string {
498566
))
499567
}
500568

569+
ifm.enableCustomInput {
570+
// Add the "+ Add custom value" option at the bottom
571+
cursor:=" "
572+
text:=" + Add custom value"
573+
ifm.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
returns.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+
gofunc() {
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+
funcnewMultiSelectWithCustomInput(ptty*ptytest.PTY,items []string) ([]string,error) {
120+
varvalues []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+
iferr==nil {
129+
values=selectedItems
130+
}
131+
returnerr
132+
},
133+
}
134+
inv:=cmd.Invoke()
135+
ptty.Attach(inv)
136+
returnvalues,inv.Run()
104137
}
105138

106139
funcnewMultiSelect(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)

[8]ページ先頭

©2009-2025 Movatter.jp