Skip to content

Commit d1c5baf

Browse files
committed
Merge remote-tracking branch 'origin/main' into jjs/dau-history
2 parents d6c5a4f + 14579fa commit d1c5baf

File tree

4,075 files changed

+360
-450
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

4,075 files changed

+360
-450
lines changed

Makefile

+7-4
Original file line numberDiff line numberDiff line change
@@ -422,7 +422,9 @@ fmt/go:
422422
echo "$(GREEN)==>$(RESET) $(BOLD)fmt/go$(RESET)"
423423
# VS Code users should check out
424424
# https://github.com/mvdan/gofumpt#visual-studio-code
425-
go run mvdan.cc/gofumpt@v0.4.0 -w -l .
425+
find . $(FIND_EXCLUSIONS) -type f -name '*.go' -print0 | \
426+
xargs -0 grep --null -L "DO NOT EDIT" | \
427+
xargs -0 go run mvdan.cc/gofumpt@v0.4.0 -w -l
426428
.PHONY: fmt/go
427429

428430
fmt/ts:
@@ -511,9 +513,7 @@ TAILNETTEST_MOCKS := \
511513
tailnet/tailnettest/workspaceupdatesprovidermock.go \
512514
tailnet/tailnettest/subscriptionmock.go
513515

514-
515-
# all gen targets should be added here and to gen/mark-fresh
516-
gen: \
516+
GEN_FILES := \
517517
tailnet/proto/tailnet.pb.go \
518518
agent/proto/agent.pb.go \
519519
provisionersdk/proto/provisioner.pb.go \
@@ -538,6 +538,9 @@ gen: \
538538
examples/examples.gen.json \
539539
$(TAILNETTEST_MOCKS) \
540540
coderd/database/pubsub/psmock/psmock.go
541+
542+
# all gen targets should be added here and to gen/mark-fresh
543+
gen: $(GEN_FILES)
541544
.PHONY: gen
542545

543546
# Mark all generated files as fresh so make thinks they're up-to-date. This is

cli/cliui/select.go

+132-17
Original file line numberDiff line numberDiff line change
@@ -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+
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
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.isCustomInputMode {
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.isCustomInputMode = true
413+
return m, nil
414+
}
401415
if len(m.options) != 0 {
402416
m.selected = true
403417
return m, tea.Quit
@@ -413,16 +427,16 @@ func (m multiSelectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
413427
return m, nil
414428

415429
case tea.KeyUp:
416-
options := m.filteredOptions()
430+
maxIndex := m.getMaxIndex()
417431
if m.cursor > 0 {
418432
m.cursor--
419433
} else {
420-
m.cursor = len(options) - 1
434+
m.cursor = maxIndex
421435
}
422436

423437
case tea.KeyDown:
424-
options := m.filteredOptions()
425-
if m.cursor < len(options)-1 {
438+
maxIndex := m.getMaxIndex()
439+
if m.cursor < maxIndex {
426440
m.cursor++
427441
} else {
428442
m.cursor = 0
@@ -457,6 +471,91 @@ func (m multiSelectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
457471
return m, cmd
458472
}
459473

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+
460559
func (m multiSelectModel) View() string {
461560
var s strings.Builder
462561

@@ -469,13 +568,19 @@ func (m multiSelectModel) View() string {
469568
return s.String()
470569
}
471570

571+
if m.isCustomInputMode {
572+
_, _ = s.WriteString(fmt.Sprintf("%s\nEnter custom value: %s\n", msg, m.customInput))
573+
return s.String()
574+
}
575+
472576
_, _ = s.WriteString(fmt.Sprintf(
473577
"%s %s[Use arrows to move, space to select, <right> to all, <left> to none, type to filter]\n",
474578
msg,
475579
m.search.View(),
476580
))
477581

478-
for i, option := range m.filteredOptions() {
582+
options := m.filteredOptions()
583+
for i, option := range options {
479584
cursor := " "
480585
chosen := "[ ]"
481586
o := option.option
@@ -498,6 +603,16 @@ func (m multiSelectModel) View() string {
498603
))
499604
}
500605

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+
}
501616
return s.String()
502617
}
503618

cli/cliui/select_test.go

+33
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

+13-3
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,15 @@ func (RootCmd) promptExample() *serpent.Command {
4141
Default: "",
4242
Value: serpent.StringArrayOf(&multiSelectValues),
4343
}
44+
45+
enableCustomInput bool
46+
enableCustomInputOption = serpent.Option{
47+
Name: "enable-custom-input",
48+
Description: "Enable custom input option in multi-select.",
49+
Required: false,
50+
Flag: "enable-custom-input",
51+
Value: serpent.BoolOf(&enableCustomInput),
52+
}
4453
)
4554
cmd := &serpent.Command{
4655
Use: "prompt-example",
@@ -156,14 +165,15 @@ func (RootCmd) promptExample() *serpent.Command {
156165
multiSelectValues, multiSelectError = cliui.MultiSelect(inv, cliui.MultiSelectOptions{
157166
Message: "Select some things:",
158167
Options: []string{
159-
"Code", "Chair", "Whale", "Diamond", "Carrot",
168+
"Code", "Chairs", "Whale", "Diamond", "Carrot",
160169
},
161-
Defaults: []string{"Code"},
170+
Defaults: []string{"Code"},
171+
EnableCustomInput: enableCustomInput,
162172
})
163173
}
164174
_, _ = fmt.Fprintf(inv.Stdout, "%q are nice choices.\n", strings.Join(multiSelectValues, ", "))
165175
return multiSelectError
166-
}, useThingsOption),
176+
}, useThingsOption, enableCustomInputOption),
167177
promptCmd("rich-parameter", func(inv *serpent.Invocation) error {
168178
value, err := cliui.RichSelect(inv, cliui.RichSelectOptions{
169179
Options: []codersdk.TemplateVersionParameterOption{

examples/examples.gen.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666
"linux",
6767
"digitalocean"
6868
],
69-
"markdown": "\n# Remote Development on DigitalOcean Droplets\n\nProvision DigitalOcean Droplets as [Coder workspaces](https://coder.com/docs/workspaces) with this example template.\n\n\u003c!-- TODO: Add screenshot --\u003e\n\n## Prerequisites\n\nTo deploy workspaces as DigitalOcean Droplets, you'll need:\n\n- DigitalOcean [personal access token (PAT)](https://docs.digitalocean.com/reference/api/create-personal-access-token/)\n\n- DigitalOcean project ID (you can get your project information via the `doctl`\n CLI by running `doctl projects list`)\n\n- Remove the following sections from the `main.tf` file if you don't want to\n associate your workspaces with a project:\n\n - `variable \"step2_do_project_id\"`\n - `resource \"digitalocean_project_resources\" \"project\"`\n\n- **Optional:** DigitalOcean SSH key ID (obtain via the `doctl` CLI by running\n `doctl compute ssh-key list`)\n\n- Note that this is only required for Fedora images to work.\n\n### Authentication\n\nThis template assumes that coderd is run in an environment that is authenticated\nwith Digital Ocean. Obtain a [Digital Ocean Personal Access\nToken](https://cloud.digitalocean.com/account/api/tokens) and set the\nenvironment variable `DIGITALOCEAN_TOKEN` to the access token before starting\ncoderd. For other ways to authenticate [consult the Terraform docs](https://registry.terraform.io/providers/digitalocean/digitalocean/latest/docs).\n\n## Architecture\n\nThis template provisions the following resources:\n\n- Azure VM (ephemeral, deleted on stop)\n- Managed disk (persistent, mounted to `/home/coder`)\n\nThis means, when the workspace restarts, any tools or files outside of the home directory are not persisted. To pre-bake tools into the workspace (e.g. `python3`), modify the VM image, or use a [startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/script).\n\n\u003e **Note**\n\u003e This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.\n"
69+
"markdown": "\n# Remote Development on DigitalOcean Droplets\n\nProvision DigitalOcean Droplets as [Coder workspaces](https://coder.com/docs/workspaces) with this example template.\n\n\u003c!-- TODO: Add screenshot --\u003e\n\n## Prerequisites\n\nTo deploy workspaces as DigitalOcean Droplets, you'll need:\n\n- DigitalOcean [personal access token (PAT)](https://docs.digitalocean.com/reference/api/create-personal-access-token)\n\n- DigitalOcean project ID (you can get your project information via the `doctl` CLI by running `doctl projects list`)\n\n - Remove the following sections from the `main.tf` file if you don't want to\n associate your workspaces with a project:\n\n - `variable \"project_uuid\"`\n - `resource \"digitalocean_project_resources\" \"project\"`\n\n- **Optional:** DigitalOcean SSH key ID (obtain via the `doctl` CLI by running\n `doctl compute ssh-key list`)\n\n - Note that this is only required for Fedora images to work.\n\n### Authentication\n\nThis template assumes that the Coder Provisioner is run in an environment that is authenticated with Digital Ocean.\n\nObtain a [Digital Ocean Personal Access Token](https://cloud.digitalocean.com/account/api/tokens) and set the `DIGITALOCEAN_TOKEN` environment variable to the access token.\nFor other ways to authenticate [consult the Terraform provider's docs](https://registry.terraform.io/providers/digitalocean/digitalocean/latest/docs).\n\n## Architecture\n\nThis template provisions the following resources:\n\n- DigitalOcean VM (ephemeral, deleted on stop)\n- Managed disk (persistent, mounted to `/home/coder`)\n\nThis means, when the workspace restarts, any tools or files outside of the home directory are not persisted. To pre-bake tools into the workspace (e.g. `python3`), modify the VM image, or use a [startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/script).\n\n\u003e [!NOTE]\n\u003e This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.\n"
7070
},
7171
{
7272
"id": "docker",

examples/templates/digitalocean-linux/README.md

+13-15
Original file line numberDiff line numberDiff line change
@@ -17,38 +17,36 @@ Provision DigitalOcean Droplets as [Coder workspaces](https://coder.com/docs/wor
1717

1818
To deploy workspaces as DigitalOcean Droplets, you'll need:
1919

20-
- DigitalOcean [personal access token (PAT)](https://docs.digitalocean.com/reference/api/create-personal-access-token/)
20+
- DigitalOcean [personal access token (PAT)](https://docs.digitalocean.com/reference/api/create-personal-access-token)
2121

22-
- DigitalOcean project ID (you can get your project information via the `doctl`
23-
CLI by running `doctl projects list`)
22+
- DigitalOcean project ID (you can get your project information via the `doctl` CLI by running `doctl projects list`)
2423

25-
- Remove the following sections from the `main.tf` file if you don't want to
26-
associate your workspaces with a project:
24+
- Remove the following sections from the `main.tf` file if you don't want to
25+
associate your workspaces with a project:
2726

28-
- `variable "step2_do_project_id"`
29-
- `resource "digitalocean_project_resources" "project"`
27+
- `variable "project_uuid"`
28+
- `resource "digitalocean_project_resources" "project"`
3029

3130
- **Optional:** DigitalOcean SSH key ID (obtain via the `doctl` CLI by running
3231
`doctl compute ssh-key list`)
3332

34-
- Note that this is only required for Fedora images to work.
33+
- Note that this is only required for Fedora images to work.
3534

3635
### Authentication
3736

38-
This template assumes that coderd is run in an environment that is authenticated
39-
with Digital Ocean. Obtain a [Digital Ocean Personal Access
40-
Token](https://cloud.digitalocean.com/account/api/tokens) and set the
41-
environment variable `DIGITALOCEAN_TOKEN` to the access token before starting
42-
coderd. For other ways to authenticate [consult the Terraform docs](https://registry.terraform.io/providers/digitalocean/digitalocean/latest/docs).
37+
This template assumes that the Coder Provisioner is run in an environment that is authenticated with Digital Ocean.
38+
39+
Obtain a [Digital Ocean Personal Access Token](https://cloud.digitalocean.com/account/api/tokens) and set the `DIGITALOCEAN_TOKEN` environment variable to the access token.
40+
For other ways to authenticate [consult the Terraform provider's docs](https://registry.terraform.io/providers/digitalocean/digitalocean/latest/docs).
4341

4442
## Architecture
4543

4644
This template provisions the following resources:
4745

48-
- Azure VM (ephemeral, deleted on stop)
46+
- DigitalOcean VM (ephemeral, deleted on stop)
4947
- Managed disk (persistent, mounted to `/home/coder`)
5048

5149
This means, when the workspace restarts, any tools or files outside of the home directory are not persisted. To pre-bake tools into the workspace (e.g. `python3`), modify the VM image, or use a [startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/script).
5250

53-
> **Note**
51+
> [!NOTE]
5452
> This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.

0 commit comments

Comments
 (0)