From ce4b3c2b349daaa98c19ce8e2517b1720117c3b1 Mon Sep 17 00:00:00 2001 From: joobisb Date: Thu, 19 Dec 2024 16:29:09 +0530 Subject: [PATCH 1/4] feat: allow entering non-default values in multi-select --- cli/cliui/select.go | 110 +++++++++++++++++++++++++++++++++------ cli/cliui/select_test.go | 33 ++++++++++++ cli/prompts.go | 5 +- 3 files changed, 130 insertions(+), 18 deletions(-) diff --git a/cli/cliui/select.go b/cli/cliui/select.go index 39e547c0258ea..8d5b2886f4082 100644 --- a/cli/cliui/select.go +++ b/cli/cliui/select.go @@ -195,7 +195,7 @@ func (m selectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if m.cursor > 0 { m.cursor-- } else { - m.cursor = len(options) - 1 + m.cursor = len(options) } case tea.KeyDown: @@ -300,9 +300,10 @@ func (m selectModel) filteredOptions() []string { } type MultiSelectOptions struct { - Message string - Options []string - Defaults []string + Message string + Options []string + Defaults []string + EnableCustomInput bool } func MultiSelect(inv *serpent.Invocation, opts MultiSelectOptions) ([]string, error) { @@ -328,9 +329,10 @@ func MultiSelect(inv *serpent.Invocation, opts MultiSelectOptions) ([]string, er } initialModel := multiSelectModel{ - search: textinput.New(), - options: options, - message: opts.Message, + search: textinput.New(), + options: options, + message: opts.Message, + enableCustomInput: opts.EnableCustomInput, } initialModel.search.Prompt = "" @@ -370,12 +372,15 @@ type multiSelectOption struct { } type multiSelectModel struct { - search textinput.Model - options []*multiSelectOption - cursor int - message string - canceled bool - selected bool + search textinput.Model + options []*multiSelectOption + cursor int + message string + canceled bool + selected bool + isInputMode bool // New field to track if we're adding a custom option + customInput string // New field to store custom input + enableCustomInput bool // New field to control whether custom input is allowed } func (multiSelectModel) Init() tea.Cmd { @@ -386,6 +391,10 @@ func (multiSelectModel) Init() tea.Cmd { func (m multiSelectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd + if m.isInputMode { + return m.handleCustomInputMode(msg) + } + switch msg := msg.(type) { case terminateMsg: m.canceled = true @@ -398,6 +407,11 @@ func (m multiSelectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, tea.Quit case tea.KeyEnter: + // Switch to custom input mode if we're on the "+ Add custom value:" option + if m.enableCustomInput && m.cursor == len(m.filteredOptions()) { + m.isInputMode = true + return m, nil + } if len(m.options) != 0 { m.selected = true return m, tea.Quit @@ -414,15 +428,17 @@ func (m multiSelectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.KeyUp: options := m.filteredOptions() + maxIndex := len(options) if m.cursor > 0 { m.cursor-- } else { - m.cursor = len(options) - 1 + m.cursor = maxIndex } case tea.KeyDown: options := m.filteredOptions() - if m.cursor < len(options)-1 { + maxIndex := len(options) + if m.cursor < maxIndex { m.cursor++ } else { m.cursor = 0 @@ -457,6 +473,52 @@ func (m multiSelectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, cmd } +// handleCustomInputMode manages keyboard interactions when in custom input mode +func (m *multiSelectModel) handleCustomInputMode(msg tea.Msg) (tea.Model, tea.Cmd) { + keyMsg, ok := msg.(tea.KeyMsg) + if !ok { + return m, nil + } + + switch keyMsg.Type { + case tea.KeyEnter: + return m.handleCustomInputSubmission() + + case tea.KeyCtrlC: + m.canceled = true + return m, tea.Quit + + case tea.KeyBackspace: + return m.handleCustomInputBackspace() + + default: + m.customInput += keyMsg.String() + return m, nil + } +} + +// handleCustomInputSubmission processes the submission of custom input +func (m *multiSelectModel) handleCustomInputSubmission() (tea.Model, tea.Cmd) { + if m.customInput != "" { + m.options = append(m.options, &multiSelectOption{ + option: m.customInput, + chosen: true, + }) + } + // Reset input state regardless of whether input was empty + m.customInput = "" + m.isInputMode = false + return m, nil +} + +// handleCustomInputBackspace handles backspace in custom input mode +func (m *multiSelectModel) handleCustomInputBackspace() (tea.Model, tea.Cmd) { + if len(m.customInput) > 0 { + m.customInput = m.customInput[:len(m.customInput)-1] + } + return m, nil +} + func (m multiSelectModel) View() string { var s strings.Builder @@ -469,13 +531,19 @@ func (m multiSelectModel) View() string { return s.String() } + if m.isInputMode { + _, _ = s.WriteString(fmt.Sprintf("%s\nEnter custom value: %s\n", msg, m.customInput)) + return s.String() + } + _, _ = s.WriteString(fmt.Sprintf( "%s %s[Use arrows to move, space to select, to all, to none, type to filter]\n", msg, m.search.View(), )) - for i, option := range m.filteredOptions() { + options := m.filteredOptions() + for i, option := range options { cursor := " " chosen := "[ ]" o := option.option @@ -498,6 +566,16 @@ func (m multiSelectModel) View() string { )) } + if m.enableCustomInput { + // Add the "+ Add custom value" option at the bottom + cursor := " " + text := " + Add custom value" + if m.cursor == len(options) { + cursor = pretty.Sprint(DefaultStyles.Keyword, "> ") + text = pretty.Sprint(DefaultStyles.Keyword, text) + } + _, _ = s.WriteString(fmt.Sprintf("%s%s\n", cursor, text)) + } return s.String() } diff --git a/cli/cliui/select_test.go b/cli/cliui/select_test.go index c0da49714fc40..c7630ac4f2460 100644 --- a/cli/cliui/select_test.go +++ b/cli/cliui/select_test.go @@ -101,6 +101,39 @@ func TestMultiSelect(t *testing.T) { }() require.Equal(t, items, <-msgChan) }) + + t.Run("MultiSelectWithCustomInput", func(t *testing.T) { + t.Parallel() + items := []string{"Code", "Chairs", "Whale", "Diamond", "Carrot"} + ptty := ptytest.New(t) + msgChan := make(chan []string) + go func() { + resp, err := newMultiSelectWithCustomInput(ptty, items) + assert.NoError(t, err) + msgChan <- resp + }() + require.Equal(t, items, <-msgChan) + }) +} + +func newMultiSelectWithCustomInput(ptty *ptytest.PTY, items []string) ([]string, error) { + var values []string + cmd := &serpent.Command{ + Handler: func(inv *serpent.Invocation) error { + selectedItems, err := cliui.MultiSelect(inv, cliui.MultiSelectOptions{ + Options: items, + Defaults: items, + EnableCustomInput: true, + }) + if err == nil { + values = selectedItems + } + return err + }, + } + inv := cmd.Invoke() + ptty.Attach(inv) + return values, inv.Run() } func newMultiSelect(ptty *ptytest.PTY, items []string) ([]string, error) { diff --git a/cli/prompts.go b/cli/prompts.go index 9bd7ecaa03204..1ee47e7ea1abf 100644 --- a/cli/prompts.go +++ b/cli/prompts.go @@ -156,9 +156,10 @@ func (RootCmd) promptExample() *serpent.Command { multiSelectValues, multiSelectError = cliui.MultiSelect(inv, cliui.MultiSelectOptions{ Message: "Select some things:", Options: []string{ - "Code", "Chair", "Whale", "Diamond", "Carrot", + "Code", "Chairs", "Whale", "Diamond", "Carrot", }, - Defaults: []string{"Code"}, + Defaults: []string{"Code"}, + EnableCustomInput: true, }) } _, _ = fmt.Fprintf(inv.Stdout, "%q are nice choices.\n", strings.Join(multiSelectValues, ", ")) From cc41fe79798c70437cadadb4bcac37e831f671ea Mon Sep 17 00:00:00 2001 From: joobisb Date: Fri, 20 Dec 2024 11:42:20 +0530 Subject: [PATCH 2/4] fix: duplicates, cursor positioning --- cli/cliui/select.go | 67 +++++++++++++++++++++++++++++++++++---------- cli/prompts.go | 13 +++++++-- 2 files changed, 63 insertions(+), 17 deletions(-) diff --git a/cli/cliui/select.go b/cli/cliui/select.go index 8d5b2886f4082..be8b6ed0abf12 100644 --- a/cli/cliui/select.go +++ b/cli/cliui/select.go @@ -378,7 +378,7 @@ type multiSelectModel struct { message string canceled bool selected bool - isInputMode bool // New field to track if we're adding a custom option + isCustomInputMode bool // New field to track if we're adding a custom option customInput string // New field to store custom input enableCustomInput bool // New field to control whether custom input is allowed } @@ -391,7 +391,7 @@ func (multiSelectModel) Init() tea.Cmd { func (m multiSelectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd - if m.isInputMode { + if m.isCustomInputMode { return m.handleCustomInputMode(msg) } @@ -409,7 +409,7 @@ func (m multiSelectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.KeyEnter: // Switch to custom input mode if we're on the "+ Add custom value:" option if m.enableCustomInput && m.cursor == len(m.filteredOptions()) { - m.isInputMode = true + m.isCustomInputMode = true return m, nil } if len(m.options) != 0 { @@ -427,8 +427,7 @@ func (m multiSelectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil case tea.KeyUp: - options := m.filteredOptions() - maxIndex := len(options) + maxIndex := m.getMaxIndex() if m.cursor > 0 { m.cursor-- } else { @@ -436,8 +435,7 @@ func (m multiSelectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } case tea.KeyDown: - options := m.filteredOptions() - maxIndex := len(options) + maxIndex := m.getMaxIndex() if m.cursor < maxIndex { m.cursor++ } else { @@ -473,6 +471,16 @@ func (m multiSelectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, cmd } +func (m multiSelectModel) getMaxIndex() int { + options := m.filteredOptions() + if m.enableCustomInput { + // Include the "+ Add custom value" entry + return len(options) + } + // Includes only the actual options + return len(options) - 1 +} + // handleCustomInputMode manages keyboard interactions when in custom input mode func (m *multiSelectModel) handleCustomInputMode(msg tea.Msg) (tea.Model, tea.Cmd) { keyMsg, ok := msg.(tea.KeyMsg) @@ -499,15 +507,44 @@ func (m *multiSelectModel) handleCustomInputMode(msg tea.Msg) (tea.Model, tea.Cm // handleCustomInputSubmission processes the submission of custom input func (m *multiSelectModel) handleCustomInputSubmission() (tea.Model, tea.Cmd) { - if m.customInput != "" { - m.options = append(m.options, &multiSelectOption{ - option: m.customInput, - chosen: true, - }) + if m.customInput == "" { + m.isCustomInputMode = false + return m, nil + } + + // Clear search to ensure option is visible and cursor points to the new option + m.search.SetValue("") + + // Check for duplicates + for i, opt := range m.options { + if strings.EqualFold(opt.option, m.customInput) { + // If the option exists but isn't chosen, select it + if !opt.chosen { + opt.chosen = true + } + + // Point cursor to the new option + m.cursor = i + + // Reset custom input mode to disabled + m.isCustomInputMode = false + m.customInput = "" + return m, nil + } } - // Reset input state regardless of whether input was empty + + // Add new unique option + m.options = append(m.options, &multiSelectOption{ + option: m.customInput, + chosen: true, + }) + + // Point cursor to the newly added option + m.cursor = len(m.options) - 1 + + // Reset custom input mode to disabled m.customInput = "" - m.isInputMode = false + m.isCustomInputMode = false return m, nil } @@ -531,7 +568,7 @@ func (m multiSelectModel) View() string { return s.String() } - if m.isInputMode { + if m.isCustomInputMode { _, _ = s.WriteString(fmt.Sprintf("%s\nEnter custom value: %s\n", msg, m.customInput)) return s.String() } diff --git a/cli/prompts.go b/cli/prompts.go index 1ee47e7ea1abf..225685a0c375a 100644 --- a/cli/prompts.go +++ b/cli/prompts.go @@ -41,6 +41,15 @@ func (RootCmd) promptExample() *serpent.Command { Default: "", Value: serpent.StringArrayOf(&multiSelectValues), } + + enableCustomInput bool + enableCustomInputOption = serpent.Option{ + Name: "enable-custom-input", + Description: "Enable custom input option in multi-select.", + Required: false, + Flag: "enable-custom-input", + Value: serpent.BoolOf(&enableCustomInput), + } ) cmd := &serpent.Command{ Use: "prompt-example", @@ -159,12 +168,12 @@ func (RootCmd) promptExample() *serpent.Command { "Code", "Chairs", "Whale", "Diamond", "Carrot", }, Defaults: []string{"Code"}, - EnableCustomInput: true, + EnableCustomInput: enableCustomInput, }) } _, _ = fmt.Fprintf(inv.Stdout, "%q are nice choices.\n", strings.Join(multiSelectValues, ", ")) return multiSelectError - }, useThingsOption), + }, useThingsOption, enableCustomInputOption), promptCmd("rich-parameter", func(inv *serpent.Invocation) error { value, err := cliui.RichSelect(inv, cliui.RichSelectOptions{ Options: []codersdk.TemplateVersionParameterOption{ From 3a3ee034ea7ebab9d883074a664e4031cff15ac7 Mon Sep 17 00:00:00 2001 From: joobisb Date: Fri, 20 Dec 2024 11:49:04 +0530 Subject: [PATCH 3/4] chore: remove unwanted change --- cli/cliui/select.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cli/cliui/select.go b/cli/cliui/select.go index be8b6ed0abf12..212efb346acd5 100644 --- a/cli/cliui/select.go +++ b/cli/cliui/select.go @@ -195,7 +195,7 @@ func (m selectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if m.cursor > 0 { m.cursor-- } else { - m.cursor = len(options) + m.cursor = len(options) - 1 } case tea.KeyDown: @@ -378,9 +378,9 @@ type multiSelectModel struct { message string canceled bool selected bool - isCustomInputMode bool // New field to track if we're adding a custom option - customInput string // New field to store custom input - enableCustomInput bool // New field to control whether custom input is allowed + isCustomInputMode bool // track if we're adding a custom option + customInput string // store custom input + enableCustomInput bool // control whether custom input is allowed } func (multiSelectModel) Init() tea.Cmd { From 37e2dd43460773a598e0ee1865a0ed5563c00648 Mon Sep 17 00:00:00 2001 From: joobisb Date: Fri, 20 Dec 2024 16:11:27 +0530 Subject: [PATCH 4/4] remove case insensitive matching --- cli/cliui/select.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/cliui/select.go b/cli/cliui/select.go index 212efb346acd5..4697dda09d660 100644 --- a/cli/cliui/select.go +++ b/cli/cliui/select.go @@ -517,7 +517,7 @@ func (m *multiSelectModel) handleCustomInputSubmission() (tea.Model, tea.Cmd) { // Check for duplicates for i, opt := range m.options { - if strings.EqualFold(opt.option, m.customInput) { + if opt.option == m.customInput { // If the option exists but isn't chosen, select it if !opt.chosen { opt.chosen = true 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