Add experimental huh-only prompter gated by GH_EXPERIMENTAL_PROMPTER
Introduce a new Prompter implementation (huhPrompter) that uses the charmbracelet/huh library in its standard interactive mode, as an alternative to the survey-based default prompter. The new implementation is gated behind the GH_EXPERIMENTAL_PROMPTER environment variable, following the same truthy/falsey pattern as GH_ACCESSIBLE_PROMPTER. Key differences from the accessible prompter: - No WithAccessible(true) flag (full interactive TUI) - Uses EchoModePassword (masked with *) instead of EchoModeNone - No default value annotations appended to prompt text Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
39407e65e3
commit
87426ee236
4 changed files with 253 additions and 2 deletions
226
internal/prompter/huh_prompter.go
Normal file
226
internal/prompter/huh_prompter.go
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
package prompter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/cli/cli/v2/internal/ghinstance"
|
||||
"github.com/cli/cli/v2/pkg/surveyext"
|
||||
ghPrompter "github.com/cli/go-gh/v2/pkg/prompter"
|
||||
)
|
||||
|
||||
type huhPrompter struct {
|
||||
stdin ghPrompter.FileReader
|
||||
stdout ghPrompter.FileWriter
|
||||
stderr ghPrompter.FileWriter
|
||||
editorCmd string
|
||||
}
|
||||
|
||||
func (p *huhPrompter) newForm(groups ...*huh.Group) *huh.Form {
|
||||
return huh.NewForm(groups...).
|
||||
WithTheme(huh.ThemeBase16()).
|
||||
WithInput(p.stdin).
|
||||
WithOutput(p.stdout)
|
||||
}
|
||||
|
||||
func (p *huhPrompter) Select(prompt, defaultValue string, options []string) (int, error) {
|
||||
var result int
|
||||
|
||||
if !slices.Contains(options, defaultValue) {
|
||||
defaultValue = ""
|
||||
}
|
||||
|
||||
formOptions := make([]huh.Option[int], len(options))
|
||||
for i, o := range options {
|
||||
if defaultValue == o {
|
||||
result = i
|
||||
}
|
||||
formOptions[i] = huh.NewOption(o, i)
|
||||
}
|
||||
|
||||
err := p.newForm(
|
||||
huh.NewGroup(
|
||||
huh.NewSelect[int]().
|
||||
Title(prompt).
|
||||
Value(&result).
|
||||
Options(formOptions...),
|
||||
),
|
||||
).Run()
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (p *huhPrompter) MultiSelect(prompt string, defaults []string, options []string) ([]int, error) {
|
||||
var result []int
|
||||
|
||||
defaults = slices.DeleteFunc(defaults, func(s string) bool {
|
||||
return !slices.Contains(options, s)
|
||||
})
|
||||
|
||||
formOptions := make([]huh.Option[int], len(options))
|
||||
for i, o := range options {
|
||||
if slices.Contains(defaults, o) {
|
||||
result = append(result, i)
|
||||
}
|
||||
formOptions[i] = huh.NewOption(o, i)
|
||||
}
|
||||
|
||||
err := p.newForm(
|
||||
huh.NewGroup(
|
||||
huh.NewMultiSelect[int]().
|
||||
Title(prompt).
|
||||
Value(&result).
|
||||
Limit(len(options)).
|
||||
Options(formOptions...),
|
||||
),
|
||||
).Run()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (p *huhPrompter) MultiSelectWithSearch(prompt, searchPrompt string, defaultValues, persistentValues []string, searchFunc func(string) MultiSelectSearchResult) ([]string, error) {
|
||||
return multiSelectWithSearch(p, prompt, searchPrompt, defaultValues, persistentValues, searchFunc)
|
||||
}
|
||||
|
||||
func (p *huhPrompter) Input(prompt, defaultValue string) (string, error) {
|
||||
result := defaultValue
|
||||
|
||||
err := p.newForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().
|
||||
Title(prompt).
|
||||
Value(&result),
|
||||
),
|
||||
).Run()
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (p *huhPrompter) Password(prompt string) (string, error) {
|
||||
var result string
|
||||
|
||||
err := p.newForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().
|
||||
EchoMode(huh.EchoModePassword).
|
||||
Title(prompt).
|
||||
Value(&result),
|
||||
),
|
||||
).Run()
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (p *huhPrompter) Confirm(prompt string, defaultValue bool) (bool, error) {
|
||||
result := defaultValue
|
||||
|
||||
err := p.newForm(
|
||||
huh.NewGroup(
|
||||
huh.NewConfirm().
|
||||
Title(prompt).
|
||||
Value(&result),
|
||||
),
|
||||
).Run()
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (p *huhPrompter) AuthToken() (string, error) {
|
||||
var result string
|
||||
|
||||
err := p.newForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().
|
||||
EchoMode(huh.EchoModePassword).
|
||||
Title("Paste your authentication token:").
|
||||
Validate(func(input string) error {
|
||||
if input == "" {
|
||||
return fmt.Errorf("token is required")
|
||||
}
|
||||
return nil
|
||||
}).
|
||||
Value(&result),
|
||||
),
|
||||
).Run()
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (p *huhPrompter) ConfirmDeletion(requiredValue string) error {
|
||||
return p.newForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().
|
||||
Title(fmt.Sprintf("Type %q to confirm deletion", requiredValue)).
|
||||
Validate(func(input string) error {
|
||||
if input != requiredValue {
|
||||
return fmt.Errorf("You entered: %q", input)
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
),
|
||||
).Run()
|
||||
}
|
||||
|
||||
func (p *huhPrompter) InputHostname() (string, error) {
|
||||
var result string
|
||||
|
||||
err := p.newForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().
|
||||
Title("Hostname:").
|
||||
Validate(ghinstance.HostnameValidator).
|
||||
Value(&result),
|
||||
),
|
||||
).Run()
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (p *huhPrompter) MarkdownEditor(prompt, defaultValue string, blankAllowed bool) (string, error) {
|
||||
var result string
|
||||
skipOption := "skip"
|
||||
launchOption := "launch"
|
||||
options := []huh.Option[string]{
|
||||
huh.NewOption(fmt.Sprintf("Launch %s", surveyext.EditorName(p.editorCmd)), launchOption),
|
||||
}
|
||||
if blankAllowed {
|
||||
options = append(options, huh.NewOption("Skip", skipOption))
|
||||
}
|
||||
|
||||
err := p.newForm(
|
||||
huh.NewGroup(
|
||||
huh.NewSelect[string]().
|
||||
Title(prompt).
|
||||
Options(options...).
|
||||
Value(&result),
|
||||
),
|
||||
).Run()
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if result == skipOption {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
text, err := surveyext.Edit(p.editorCmd, "*.md", defaultValue, p.stdin, p.stdout, p.stderr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return text, nil
|
||||
}
|
||||
|
|
@ -53,6 +53,15 @@ type Prompter interface {
|
|||
}
|
||||
|
||||
func New(editorCmd string, io *iostreams.IOStreams) Prompter {
|
||||
if io.ExperimentalPrompterEnabled() {
|
||||
return &huhPrompter{
|
||||
stdin: io.In,
|
||||
stdout: io.Out,
|
||||
stderr: io.ErrOut,
|
||||
editorCmd: editorCmd,
|
||||
}
|
||||
}
|
||||
|
||||
if io.AccessiblePrompterEnabled() {
|
||||
return &accessiblePrompter{
|
||||
stdin: io.In,
|
||||
|
|
|
|||
|
|
@ -316,6 +316,13 @@ func ioStreams(f *cmdutil.Factory) *iostreams.IOStreams {
|
|||
io.SetAccessiblePrompterEnabled(true)
|
||||
}
|
||||
|
||||
experimentalPrompterValue, experimentalPrompterIsSet := os.LookupEnv("GH_EXPERIMENTAL_PROMPTER")
|
||||
if experimentalPrompterIsSet {
|
||||
if !slices.Contains(falseyValues, experimentalPrompterValue) {
|
||||
io.SetExperimentalPrompterEnabled(true)
|
||||
}
|
||||
}
|
||||
|
||||
ghSpinnerDisabledValue, ghSpinnerDisabledIsSet := os.LookupEnv("GH_SPINNER_DISABLED")
|
||||
if ghSpinnerDisabledIsSet {
|
||||
if !slices.Contains(falseyValues, ghSpinnerDisabledValue) {
|
||||
|
|
|
|||
|
|
@ -79,8 +79,9 @@ type IOStreams struct {
|
|||
pagerCommand string
|
||||
pagerProcess *os.Process
|
||||
|
||||
neverPrompt bool
|
||||
accessiblePrompterEnabled bool
|
||||
neverPrompt bool
|
||||
accessiblePrompterEnabled bool
|
||||
experimentalPrompterEnabled bool
|
||||
|
||||
TempFileOverride *os.File
|
||||
}
|
||||
|
|
@ -466,6 +467,14 @@ func (s *IOStreams) AccessiblePrompterEnabled() bool {
|
|||
return s.accessiblePrompterEnabled
|
||||
}
|
||||
|
||||
func (s *IOStreams) SetExperimentalPrompterEnabled(enabled bool) {
|
||||
s.experimentalPrompterEnabled = enabled
|
||||
}
|
||||
|
||||
func (s *IOStreams) ExperimentalPrompterEnabled() bool {
|
||||
return s.experimentalPrompterEnabled
|
||||
}
|
||||
|
||||
func System() *IOStreams {
|
||||
terminal := ghTerm.FromEnv()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue