Ensure huh prompter cleans up

This commit is contained in:
William Martin 2026-03-26 14:07:40 +01:00
parent 84a3ba83e4
commit cb2b50576f
2 changed files with 47 additions and 10 deletions

View file

@ -1,10 +1,12 @@
package prompter
import (
"errors"
"fmt"
"slices"
"charm.land/huh/v2"
"github.com/AlecAivazis/survey/v2/terminal"
"github.com/cli/cli/v2/internal/ghinstance"
"github.com/cli/cli/v2/pkg/surveyext"
ghPrompter "github.com/cli/go-gh/v2/pkg/prompter"
@ -24,6 +26,18 @@ func (p *huhPrompter) newForm(groups ...*huh.Group) *huh.Form {
WithOutput(p.stdout)
}
func (p *huhPrompter) runForm(form *huh.Form) error {
err := form.Run()
if errors.Is(err, huh.ErrUserAborted) {
// TODO(huh-prompter-improvements)
// It's unfortunate that we take a dependency on survey/terminal here, but our clean cancellation logic
// in cmd.go expects it. Better would be to have a prompter.Cancelled sentinel error, but then we need to
// go and change non-experimental code to do so, and I don't think we should take that on right now.
return terminal.InterruptErr
}
return err
}
func (p *huhPrompter) buildSelectForm(prompt, defaultValue string, options []string) (*huh.Form, *int) {
var result int
@ -52,7 +66,7 @@ func (p *huhPrompter) buildSelectForm(prompt, defaultValue string, options []str
func (p *huhPrompter) Select(prompt, defaultValue string, options []string) (int, error) {
form, result := p.buildSelectForm(prompt, defaultValue, options)
err := form.Run()
err := p.runForm(form)
return *result, err
}
@ -85,7 +99,7 @@ func (p *huhPrompter) buildMultiSelectForm(prompt string, defaults []string, opt
func (p *huhPrompter) MultiSelect(prompt string, defaults []string, options []string) ([]int, error) {
form, result := p.buildMultiSelectForm(prompt, defaults, options)
err := form.Run()
err := p.runForm(form)
if err != nil {
return nil, err
}
@ -100,7 +114,7 @@ func (p *huhPrompter) buildMultiSelectWithSearchForm(prompt, searchPrompt string
func (p *huhPrompter) MultiSelectWithSearch(prompt, searchPrompt string, defaultValues, persistentValues []string, searchFunc func(string) MultiSelectSearchResult) ([]string, error) {
form, field := p.buildMultiSelectWithSearchForm(prompt, searchPrompt, defaultValues, persistentValues, searchFunc)
err := form.Run()
err := p.runForm(form)
if err != nil {
return nil, err
}
@ -121,7 +135,7 @@ func (p *huhPrompter) buildInputForm(prompt, defaultValue string) (*huh.Form, *s
func (p *huhPrompter) Input(prompt, defaultValue string) (string, error) {
form, result := p.buildInputForm(prompt, defaultValue)
err := form.Run()
err := p.runForm(form)
return *result, err
}
@ -140,7 +154,7 @@ func (p *huhPrompter) buildPasswordForm(prompt string) (*huh.Form, *string) {
func (p *huhPrompter) Password(prompt string) (string, error) {
form, result := p.buildPasswordForm(prompt)
err := form.Run()
err := p.runForm(form)
if err != nil {
return "", err
}
@ -161,7 +175,7 @@ func (p *huhPrompter) buildConfirmForm(prompt string, defaultValue bool) (*huh.F
func (p *huhPrompter) Confirm(prompt string, defaultValue bool) (bool, error) {
form, result := p.buildConfirmForm(prompt, defaultValue)
err := form.Run()
err := p.runForm(form)
if err != nil {
return false, err
}
@ -189,7 +203,7 @@ func (p *huhPrompter) buildAuthTokenForm() (*huh.Form, *string) {
func (p *huhPrompter) AuthToken() (string, error) {
form, result := p.buildAuthTokenForm()
err := form.Run()
err := p.runForm(form)
return *result, err
}
@ -209,7 +223,7 @@ func (p *huhPrompter) buildConfirmDeletionForm(requiredValue string) *huh.Form {
}
func (p *huhPrompter) ConfirmDeletion(requiredValue string) error {
return p.buildConfirmDeletionForm(requiredValue).Run()
return p.runForm(p.buildConfirmDeletionForm(requiredValue))
}
func (p *huhPrompter) buildInputHostnameForm() (*huh.Form, *string) {
@ -227,7 +241,7 @@ func (p *huhPrompter) buildInputHostnameForm() (*huh.Form, *string) {
func (p *huhPrompter) InputHostname() (string, error) {
form, result := p.buildInputHostnameForm()
err := form.Run()
err := p.runForm(form)
if err != nil {
return "", err
}
@ -258,7 +272,7 @@ func (p *huhPrompter) buildMarkdownEditorForm(prompt string, blankAllowed bool)
func (p *huhPrompter) MarkdownEditor(prompt, defaultValue string, blankAllowed bool) (string, error) {
form, result := p.buildMarkdownEditorForm(prompt, blankAllowed)
err := form.Run()
err := p.runForm(form)
if err != nil {
return "", err
}

View file

@ -6,6 +6,7 @@ import (
"time"
"charm.land/huh/v2"
"github.com/AlecAivazis/survey/v2/terminal"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -618,3 +619,25 @@ func TestHuhPrompterMultiSelectWithSearchBackspace(t *testing.T) {
assert.Equal(t, []string{"alice"}, result.selectedKeys())
})
}
func TestRunFormTranslatesErrUserAborted(t *testing.T) {
p := newTestHuhPrompter()
form, _ := p.buildSelectForm("Pick one:", "", []string{"a", "b", "c"})
r, w := io.Pipe()
form.WithInput(r).WithOutput(io.Discard).WithWidth(80)
errCh := make(chan error, 1)
go func() { errCh <- p.runForm(form) }()
// Send Ctrl+C to trigger huh.ErrUserAborted
_, err := w.Write([]byte{0x03})
require.NoError(t, err)
select {
case err := <-errCh:
assert.ErrorIs(t, err, terminal.InterruptErr, "expected huh.ErrUserAborted to be translated to terminal.InterruptErr")
case <-time.After(5 * time.Second):
t.Fatal("runForm did not complete in time")
}
}