Skip to content

Commit cb2b505

Browse files
committed
Ensure huh prompter cleans up
1 parent 84a3ba8 commit cb2b505

2 files changed

Lines changed: 47 additions & 10 deletions

File tree

internal/prompter/huh_prompter.go

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package prompter
22

33
import (
4+
"errors"
45
"fmt"
56
"slices"
67

78
"charm.land/huh/v2"
9+
"github.com/AlecAivazis/survey/v2/terminal"
810
"github.com/cli/cli/v2/internal/ghinstance"
911
"github.com/cli/cli/v2/pkg/surveyext"
1012
ghPrompter "github.com/cli/go-gh/v2/pkg/prompter"
@@ -24,6 +26,18 @@ func (p *huhPrompter) newForm(groups ...*huh.Group) *huh.Form {
2426
WithOutput(p.stdout)
2527
}
2628

29+
func (p *huhPrompter) runForm(form *huh.Form) error {
30+
err := form.Run()
31+
if errors.Is(err, huh.ErrUserAborted) {
32+
// TODO(huh-prompter-improvements)
33+
// It's unfortunate that we take a dependency on survey/terminal here, but our clean cancellation logic
34+
// in cmd.go expects it. Better would be to have a prompter.Cancelled sentinel error, but then we need to
35+
// go and change non-experimental code to do so, and I don't think we should take that on right now.
36+
return terminal.InterruptErr
37+
}
38+
return err
39+
}
40+
2741
func (p *huhPrompter) buildSelectForm(prompt, defaultValue string, options []string) (*huh.Form, *int) {
2842
var result int
2943

@@ -52,7 +66,7 @@ func (p *huhPrompter) buildSelectForm(prompt, defaultValue string, options []str
5266

5367
func (p *huhPrompter) Select(prompt, defaultValue string, options []string) (int, error) {
5468
form, result := p.buildSelectForm(prompt, defaultValue, options)
55-
err := form.Run()
69+
err := p.runForm(form)
5670
return *result, err
5771
}
5872

@@ -85,7 +99,7 @@ func (p *huhPrompter) buildMultiSelectForm(prompt string, defaults []string, opt
8599

86100
func (p *huhPrompter) MultiSelect(prompt string, defaults []string, options []string) ([]int, error) {
87101
form, result := p.buildMultiSelectForm(prompt, defaults, options)
88-
err := form.Run()
102+
err := p.runForm(form)
89103
if err != nil {
90104
return nil, err
91105
}
@@ -100,7 +114,7 @@ func (p *huhPrompter) buildMultiSelectWithSearchForm(prompt, searchPrompt string
100114

101115
func (p *huhPrompter) MultiSelectWithSearch(prompt, searchPrompt string, defaultValues, persistentValues []string, searchFunc func(string) MultiSelectSearchResult) ([]string, error) {
102116
form, field := p.buildMultiSelectWithSearchForm(prompt, searchPrompt, defaultValues, persistentValues, searchFunc)
103-
err := form.Run()
117+
err := p.runForm(form)
104118
if err != nil {
105119
return nil, err
106120
}
@@ -121,7 +135,7 @@ func (p *huhPrompter) buildInputForm(prompt, defaultValue string) (*huh.Form, *s
121135

122136
func (p *huhPrompter) Input(prompt, defaultValue string) (string, error) {
123137
form, result := p.buildInputForm(prompt, defaultValue)
124-
err := form.Run()
138+
err := p.runForm(form)
125139
return *result, err
126140
}
127141

@@ -140,7 +154,7 @@ func (p *huhPrompter) buildPasswordForm(prompt string) (*huh.Form, *string) {
140154

141155
func (p *huhPrompter) Password(prompt string) (string, error) {
142156
form, result := p.buildPasswordForm(prompt)
143-
err := form.Run()
157+
err := p.runForm(form)
144158
if err != nil {
145159
return "", err
146160
}
@@ -161,7 +175,7 @@ func (p *huhPrompter) buildConfirmForm(prompt string, defaultValue bool) (*huh.F
161175

162176
func (p *huhPrompter) Confirm(prompt string, defaultValue bool) (bool, error) {
163177
form, result := p.buildConfirmForm(prompt, defaultValue)
164-
err := form.Run()
178+
err := p.runForm(form)
165179
if err != nil {
166180
return false, err
167181
}
@@ -189,7 +203,7 @@ func (p *huhPrompter) buildAuthTokenForm() (*huh.Form, *string) {
189203

190204
func (p *huhPrompter) AuthToken() (string, error) {
191205
form, result := p.buildAuthTokenForm()
192-
err := form.Run()
206+
err := p.runForm(form)
193207
return *result, err
194208
}
195209

@@ -209,7 +223,7 @@ func (p *huhPrompter) buildConfirmDeletionForm(requiredValue string) *huh.Form {
209223
}
210224

211225
func (p *huhPrompter) ConfirmDeletion(requiredValue string) error {
212-
return p.buildConfirmDeletionForm(requiredValue).Run()
226+
return p.runForm(p.buildConfirmDeletionForm(requiredValue))
213227
}
214228

215229
func (p *huhPrompter) buildInputHostnameForm() (*huh.Form, *string) {
@@ -227,7 +241,7 @@ func (p *huhPrompter) buildInputHostnameForm() (*huh.Form, *string) {
227241

228242
func (p *huhPrompter) InputHostname() (string, error) {
229243
form, result := p.buildInputHostnameForm()
230-
err := form.Run()
244+
err := p.runForm(form)
231245
if err != nil {
232246
return "", err
233247
}
@@ -258,7 +272,7 @@ func (p *huhPrompter) buildMarkdownEditorForm(prompt string, blankAllowed bool)
258272

259273
func (p *huhPrompter) MarkdownEditor(prompt, defaultValue string, blankAllowed bool) (string, error) {
260274
form, result := p.buildMarkdownEditorForm(prompt, blankAllowed)
261-
err := form.Run()
275+
err := p.runForm(form)
262276
if err != nil {
263277
return "", err
264278
}

internal/prompter/huh_prompter_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"time"
77

88
"charm.land/huh/v2"
9+
"github.com/AlecAivazis/survey/v2/terminal"
910
"github.com/stretchr/testify/assert"
1011
"github.com/stretchr/testify/require"
1112
)
@@ -618,3 +619,25 @@ func TestHuhPrompterMultiSelectWithSearchBackspace(t *testing.T) {
618619
assert.Equal(t, []string{"alice"}, result.selectedKeys())
619620
})
620621
}
622+
623+
func TestRunFormTranslatesErrUserAborted(t *testing.T) {
624+
p := newTestHuhPrompter()
625+
form, _ := p.buildSelectForm("Pick one:", "", []string{"a", "b", "c"})
626+
627+
r, w := io.Pipe()
628+
form.WithInput(r).WithOutput(io.Discard).WithWidth(80)
629+
630+
errCh := make(chan error, 1)
631+
go func() { errCh <- p.runForm(form) }()
632+
633+
// Send Ctrl+C to trigger huh.ErrUserAborted
634+
_, err := w.Write([]byte{0x03})
635+
require.NoError(t, err)
636+
637+
select {
638+
case err := <-errCh:
639+
assert.ErrorIs(t, err, terminal.InterruptErr, "expected huh.ErrUserAborted to be translated to terminal.InterruptErr")
640+
case <-time.After(5 * time.Second):
641+
t.Fatal("runForm did not complete in time")
642+
}
643+
}

0 commit comments

Comments
 (0)