Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: coder/coder
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v2.18.1
Choose a base ref
...
head repository: coder/coder
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v2.18.2
Choose a head ref
  • 1 commit
  • 11 files changed
  • 3 contributors

Commits on Jan 7, 2025

  1. chore: add cherry-picks for patch 2.18.2 (#16061)

    Co-authored-by: Cian Johnston <[email protected]>
    Co-authored-by: Joobi S B <[email protected]>
    3 people authored Jan 7, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    d15c470 View commit details
149 changes: 132 additions & 17 deletions cli/cliui/select.go
Original file line number Diff line number Diff line change
@@ -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
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 {
@@ -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.isCustomInputMode {
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.isCustomInputMode = true
return m, nil
}
if len(m.options) != 0 {
m.selected = true
return m, tea.Quit
@@ -413,16 +427,16 @@ func (m multiSelectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil

case tea.KeyUp:
options := m.filteredOptions()
maxIndex := m.getMaxIndex()
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 := m.getMaxIndex()
if m.cursor < maxIndex {
m.cursor++
} else {
m.cursor = 0
@@ -457,6 +471,91 @@ 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)
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.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 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
}
}

// 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.isCustomInputMode = 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 +568,19 @@ func (m multiSelectModel) View() string {
return s.String()
}

if m.isCustomInputMode {
_, _ = 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, <right> to all, <left> 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 +603,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()
}

33 changes: 33 additions & 0 deletions cli/cliui/select_test.go
Original file line number Diff line number Diff line change
@@ -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) {
16 changes: 13 additions & 3 deletions cli/prompts.go
Original file line number Diff line number Diff line change
@@ -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",
@@ -156,14 +165,15 @@ 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: 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{
Loading