@@ -300,9 +300,10 @@ func (m selectModel) filteredOptions() []string {
300
300
}
301
301
302
302
type MultiSelectOptions struct {
303
- Message string
304
- Options []string
305
- Defaults []string
303
+ Message string
304
+ Options []string
305
+ Defaults []string
306
+ EnableCustomInput bool
306
307
}
307
308
308
309
func MultiSelect (inv * serpent.Invocation , opts MultiSelectOptions ) ([]string , error ) {
@@ -328,9 +329,10 @@ func MultiSelect(inv *serpent.Invocation, opts MultiSelectOptions) ([]string, er
328
329
}
329
330
330
331
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 ,
334
336
}
335
337
336
338
initialModel .search .Prompt = ""
@@ -370,12 +372,15 @@ type multiSelectOption struct {
370
372
}
371
373
372
374
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
+ isCustomInputMode bool // track if we're adding a custom option
382
+ customInput string // store custom input
383
+ enableCustomInput bool // control whether custom input is allowed
379
384
}
380
385
381
386
func (multiSelectModel ) Init () tea.Cmd {
@@ -386,6 +391,10 @@ func (multiSelectModel) Init() tea.Cmd {
386
391
func (m multiSelectModel ) Update (msg tea.Msg ) (tea.Model , tea.Cmd ) {
387
392
var cmd tea.Cmd
388
393
394
+ if m .isCustomInputMode {
395
+ return m .handleCustomInputMode (msg )
396
+ }
397
+
389
398
switch msg := msg .(type ) {
390
399
case terminateMsg :
391
400
m .canceled = true
@@ -398,6 +407,11 @@ func (m multiSelectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
398
407
return m , tea .Quit
399
408
400
409
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 .isCustomInputMode = true
413
+ return m , nil
414
+ }
401
415
if len (m .options ) != 0 {
402
416
m .selected = true
403
417
return m , tea .Quit
@@ -413,16 +427,16 @@ func (m multiSelectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
413
427
return m , nil
414
428
415
429
case tea .KeyUp :
416
- options := m .filteredOptions ()
430
+ maxIndex := m .getMaxIndex ()
417
431
if m .cursor > 0 {
418
432
m .cursor --
419
433
} else {
420
- m .cursor = len ( options ) - 1
434
+ m .cursor = maxIndex
421
435
}
422
436
423
437
case tea .KeyDown :
424
- options := m .filteredOptions ()
425
- if m .cursor < len ( options ) - 1 {
438
+ maxIndex := m .getMaxIndex ()
439
+ if m .cursor < maxIndex {
426
440
m .cursor ++
427
441
} else {
428
442
m .cursor = 0
@@ -457,6 +471,91 @@ func (m multiSelectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
457
471
return m , cmd
458
472
}
459
473
474
+ func (m multiSelectModel ) getMaxIndex () int {
475
+ options := m .filteredOptions ()
476
+ if m .enableCustomInput {
477
+ // Include the "+ Add custom value" entry
478
+ return len (options )
479
+ }
480
+ // Includes only the actual options
481
+ return len (options ) - 1
482
+ }
483
+
484
+ // handleCustomInputMode manages keyboard interactions when in custom input mode
485
+ func (m * multiSelectModel ) handleCustomInputMode (msg tea.Msg ) (tea.Model , tea.Cmd ) {
486
+ keyMsg , ok := msg .(tea.KeyMsg )
487
+ if ! ok {
488
+ return m , nil
489
+ }
490
+
491
+ switch keyMsg .Type {
492
+ case tea .KeyEnter :
493
+ return m .handleCustomInputSubmission ()
494
+
495
+ case tea .KeyCtrlC :
496
+ m .canceled = true
497
+ return m , tea .Quit
498
+
499
+ case tea .KeyBackspace :
500
+ return m .handleCustomInputBackspace ()
501
+
502
+ default :
503
+ m .customInput += keyMsg .String ()
504
+ return m , nil
505
+ }
506
+ }
507
+
508
+ // handleCustomInputSubmission processes the submission of custom input
509
+ func (m * multiSelectModel ) handleCustomInputSubmission () (tea.Model , tea.Cmd ) {
510
+ if m .customInput == "" {
511
+ m .isCustomInputMode = false
512
+ return m , nil
513
+ }
514
+
515
+ // Clear search to ensure option is visible and cursor points to the new option
516
+ m .search .SetValue ("" )
517
+
518
+ // Check for duplicates
519
+ for i , opt := range m .options {
520
+ if opt .option == m .customInput {
521
+ // If the option exists but isn't chosen, select it
522
+ if ! opt .chosen {
523
+ opt .chosen = true
524
+ }
525
+
526
+ // Point cursor to the new option
527
+ m .cursor = i
528
+
529
+ // Reset custom input mode to disabled
530
+ m .isCustomInputMode = false
531
+ m .customInput = ""
532
+ return m , nil
533
+ }
534
+ }
535
+
536
+ // Add new unique option
537
+ m .options = append (m .options , & multiSelectOption {
538
+ option : m .customInput ,
539
+ chosen : true ,
540
+ })
541
+
542
+ // Point cursor to the newly added option
543
+ m .cursor = len (m .options ) - 1
544
+
545
+ // Reset custom input mode to disabled
546
+ m .customInput = ""
547
+ m .isCustomInputMode = false
548
+ return m , nil
549
+ }
550
+
551
+ // handleCustomInputBackspace handles backspace in custom input mode
552
+ func (m * multiSelectModel ) handleCustomInputBackspace () (tea.Model , tea.Cmd ) {
553
+ if len (m .customInput ) > 0 {
554
+ m .customInput = m .customInput [:len (m .customInput )- 1 ]
555
+ }
556
+ return m , nil
557
+ }
558
+
460
559
func (m multiSelectModel ) View () string {
461
560
var s strings.Builder
462
561
@@ -469,13 +568,19 @@ func (m multiSelectModel) View() string {
469
568
return s .String ()
470
569
}
471
570
571
+ if m .isCustomInputMode {
572
+ _ , _ = s .WriteString (fmt .Sprintf ("%s\n Enter custom value: %s\n " , msg , m .customInput ))
573
+ return s .String ()
574
+ }
575
+
472
576
_ , _ = s .WriteString (fmt .Sprintf (
473
577
"%s %s[Use arrows to move, space to select, <right> to all, <left> to none, type to filter]\n " ,
474
578
msg ,
475
579
m .search .View (),
476
580
))
477
581
478
- for i , option := range m .filteredOptions () {
582
+ options := m .filteredOptions ()
583
+ for i , option := range options {
479
584
cursor := " "
480
585
chosen := "[ ]"
481
586
o := option .option
@@ -498,6 +603,16 @@ func (m multiSelectModel) View() string {
498
603
))
499
604
}
500
605
606
+ if m .enableCustomInput {
607
+ // Add the "+ Add custom value" option at the bottom
608
+ cursor := " "
609
+ text := " + Add custom value"
610
+ if m .cursor == len (options ) {
611
+ cursor = pretty .Sprint (DefaultStyles .Keyword , "> " )
612
+ text = pretty .Sprint (DefaultStyles .Keyword , text )
613
+ }
614
+ _ , _ = s .WriteString (fmt .Sprintf ("%s%s\n " , cursor , text ))
615
+ }
501
616
return s .String ()
502
617
}
503
618
0 commit comments