From abd573ac66e8e4abd18cdc7abaef7deb60502fc6 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Mon, 7 Apr 2025 14:40:03 -0600 Subject: [PATCH 1/6] feat(preview): add `preview prompter` command --- pkg/cmd/preview/preview.go | 20 +++ pkg/cmd/preview/prompter/prompter.go | 225 +++++++++++++++++++++++++++ pkg/cmd/root/root.go | 2 + 3 files changed, 247 insertions(+) create mode 100644 pkg/cmd/preview/preview.go create mode 100644 pkg/cmd/preview/prompter/prompter.go diff --git a/pkg/cmd/preview/preview.go b/pkg/cmd/preview/preview.go new file mode 100644 index 000000000..5c3209722 --- /dev/null +++ b/pkg/cmd/preview/preview.go @@ -0,0 +1,20 @@ +package preview + +import ( + cmdPrompter "github.com/cli/cli/v2/pkg/cmd/preview/prompter" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/spf13/cobra" +) + +func NewCmdPreview(f *cmdutil.Factory) *cobra.Command { + cmd := &cobra.Command{ + Use: "preview ", + Short: "Execute previews for gh features", + } + + cmdutil.DisableAuthCheck(cmd) + + cmd.AddCommand(cmdPrompter.NewCmdPrompter(f, nil)) + + return cmd +} diff --git a/pkg/cmd/preview/prompter/prompter.go b/pkg/cmd/preview/prompter/prompter.go new file mode 100644 index 000000000..97fffb23a --- /dev/null +++ b/pkg/cmd/preview/prompter/prompter.go @@ -0,0 +1,225 @@ +package prompter + +import ( + "fmt" + + "github.com/MakeNowJust/heredoc" + "github.com/cli/cli/v2/internal/gh" + "github.com/cli/cli/v2/internal/prompter" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/spf13/cobra" +) + +type prompterOptions struct { + IO *iostreams.IOStreams + Config func() (gh.Config, error) + + PromptsToRun []func(prompter.Prompter) error +} + +func NewCmdPrompter(f *cmdutil.Factory, runF func(*prompterOptions) error) *cobra.Command { + opts := &prompterOptions{ + IO: f.IOStreams, + Config: f.Config, + } + + const ( + selectPrompt = "select" + multiSelectPrompt = "multi-select" + inputPrompt = "input" + passwordPrompt = "password" + confirmPrompt = "confirm" + authTokenPrompt = "auth-token" + confirmDeletionPrompt = "confirm-deletion" + inputHostnamePrompt = "input-hostname" + markdownEditorPrompt = "markdown-editor" + ) + + prompterTypeFuncMap := map[string]func(prompter.Prompter) error{ + selectPrompt: runSelect, + multiSelectPrompt: runMultiSelect, + inputPrompt: runInput, + passwordPrompt: runPassword, + confirmPrompt: runConfirm, + authTokenPrompt: runAuthToken, + confirmDeletionPrompt: runConfirmDeletion, + inputHostnamePrompt: runInputHostname, + markdownEditorPrompt: runMarkdownEditor, + } + + cmd := &cobra.Command{ + Use: "prompter [prompt type]", + Short: "Execute a test program to preview the prompter", + Long: heredoc.Doc(` + Execute a test program to preview the prompter. + Without an argument, all prompts will be run. + + Available prompt types: + - select + - multi-select + - input + - password + - confirm + - auth-token + - confirm-deletion + - input-hostname + - markdown-editor + `), + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if runF != nil { + return runF(opts) + } + + if len(args) == 0 { + // All prompts + for _, f := range prompterTypeFuncMap { + opts.PromptsToRun = append(opts.PromptsToRun, f) + } + } else { + // Only the one specified + for _, arg := range args { + f, ok := prompterTypeFuncMap[arg] + if !ok { + return fmt.Errorf("unknown prompter type: %q", arg) + } + opts.PromptsToRun = append(opts.PromptsToRun, f) + } + } + + return prompterRun(opts) + }, + } + + return cmd +} + +func prompterRun(opts *prompterOptions) error { + editor, err := cmdutil.DetermineEditor(opts.Config) + if err != nil { + return err + } + + p := prompter.New(editor, opts.IO.In, opts.IO.Out, opts.IO.ErrOut) + + for _, f := range opts.PromptsToRun { + if err := f(p); err != nil { + return err + } + // Newline for readability + fmt.Println() + } + + return nil +} + +func runSelect(p prompter.Prompter) error { + fmt.Println("Demonstrating Single Select") + cuisines := []string{"Italian", "Greek", "Indian", "Japanese", "American"} + favorite, err := p.Select("Favorite cuisine?", "Italian", cuisines) + if err != nil { + return err + } + fmt.Printf("Favorite cuisine: %s\n", cuisines[favorite]) + return nil +} + +func runMultiSelect(p prompter.Prompter) error { + fmt.Println("Demonstrating Multi Select") + cuisines := []string{"Italian", "Greek", "Indian", "Japanese", "American"} + favorites, err := p.MultiSelect("Favorite cuisines?", []string{}, cuisines) + if err != nil { + return err + } + for _, f := range favorites { + fmt.Printf("Favorite cuisine: %s\n", cuisines[f]) + } + return nil +} + +func runInput(p prompter.Prompter) error { + fmt.Println("Demonstrating Text Input") + text, err := p.Input("Favorite meal?", "Breakfast") + if err != nil { + return err + } + fmt.Printf("You typed: %s\n", text) + return nil +} + +func runPassword(p prompter.Prompter) error { + fmt.Println("Demonstrating Password Input") + safeword, err := p.Password("Safe word?") + if err != nil { + return err + } + fmt.Printf("Safe word: %s\n", safeword) + return nil +} + +func runConfirm(p prompter.Prompter) error { + fmt.Println("Demonstrating Confirmation") + confirmation, err := p.Confirm("Are you sure?", true) + if err != nil { + return err + } + fmt.Printf("Confirmation: %t\n", confirmation) + return nil +} + +func runAuthToken(p prompter.Prompter) error { + fmt.Println("Demonstrating Auth Token (can't be blank)") + token, err := p.AuthToken() + if err != nil { + return err + } + fmt.Printf("Auth token: %s\n", token) + return nil +} + +func runConfirmDeletion(p prompter.Prompter) error { + fmt.Println("Demonstrating Deletion Confirmation") + err := p.ConfirmDeletion("delete-me") + if err != nil { + return err + } + fmt.Println("Item deleted") + return nil +} + +func runInputHostname(p prompter.Prompter) error { + fmt.Println("Demonstrating Hostname") + hostname, err := p.InputHostname() + if err != nil { + return err + } + fmt.Printf("Hostname: %s\n", hostname) + return nil +} + +func runMarkdownEditor(p prompter.Prompter) error { + defaultText := "default text value" + + fmt.Println("Demonstrating Markdown Editor with blanks allowed and default text") + editorText, err := p.MarkdownEditor("Edit your text:", defaultText, true) + if err != nil { + return err + } + fmt.Printf("Returned text: %s\n", editorText) + + fmt.Println("Demonstrating Markdown Editor with blanks disallowed and default text") + editorText2, err := p.MarkdownEditor("Edit your text:", defaultText, false) + if err != nil { + return err + } + fmt.Printf("Returned text: %s\n", editorText2) + + fmt.Println("Demonstrating Markdown Editor with blanks disallowed and no default text") + editorText3, err := p.MarkdownEditor("Edit your text:", "", false) + if err != nil { + return err + } + fmt.Printf("Returned text: %s\n", editorText3) + return nil +} diff --git a/pkg/cmd/root/root.go b/pkg/cmd/root/root.go index c0dad93ec..ce142b32d 100644 --- a/pkg/cmd/root/root.go +++ b/pkg/cmd/root/root.go @@ -25,6 +25,7 @@ import ( labelCmd "github.com/cli/cli/v2/pkg/cmd/label" orgCmd "github.com/cli/cli/v2/pkg/cmd/org" prCmd "github.com/cli/cli/v2/pkg/cmd/pr" + previewCmd "github.com/cli/cli/v2/pkg/cmd/preview" projectCmd "github.com/cli/cli/v2/pkg/cmd/project" releaseCmd "github.com/cli/cli/v2/pkg/cmd/release" repoCmd "github.com/cli/cli/v2/pkg/cmd/repo" @@ -139,6 +140,7 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) (*cobra.Command, cmd.AddCommand(statusCmd.NewCmdStatus(f, nil)) cmd.AddCommand(codespaceCmd.NewCmdCodespace(f)) cmd.AddCommand(projectCmd.NewCmdProject(f)) + cmd.AddCommand(previewCmd.NewCmdPreview(f)) // below here at the commands that require the "intelligent" BaseRepo resolver repoResolvingCmdFactory := *f From 54a929dcd9684dc11dc23b68f09e4ed04821faa1 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Wed, 16 Apr 2025 15:49:30 -0600 Subject: [PATCH 2/6] feat(preview): enforce fixed order for prompts --- pkg/cmd/preview/prompter/prompter.go | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/pkg/cmd/preview/prompter/prompter.go b/pkg/cmd/preview/prompter/prompter.go index 97fffb23a..5b9b91f23 100644 --- a/pkg/cmd/preview/prompter/prompter.go +++ b/pkg/cmd/preview/prompter/prompter.go @@ -48,6 +48,18 @@ func NewCmdPrompter(f *cmdutil.Factory, runF func(*prompterOptions) error) *cobr markdownEditorPrompt: runMarkdownEditor, } + allPromptsOrder := []string{ + selectPrompt, + multiSelectPrompt, + inputPrompt, + passwordPrompt, + confirmPrompt, + authTokenPrompt, + confirmDeletionPrompt, + inputHostnamePrompt, + markdownEditorPrompt, + } + cmd := &cobra.Command{ Use: "prompter [prompt type]", Short: "Execute a test program to preview the prompter", @@ -73,8 +85,9 @@ func NewCmdPrompter(f *cmdutil.Factory, runF func(*prompterOptions) error) *cobr } if len(args) == 0 { - // All prompts - for _, f := range prompterTypeFuncMap { + // All prompts, in a fixed order + for _, promptType := range allPromptsOrder { + f := prompterTypeFuncMap[promptType] opts.PromptsToRun = append(opts.PromptsToRun, f) } } else { @@ -206,14 +219,14 @@ func runMarkdownEditor(p prompter.Prompter) error { if err != nil { return err } - fmt.Printf("Returned text: %s\n", editorText) + fmt.Printf("Returned text: %s\n\n", editorText) fmt.Println("Demonstrating Markdown Editor with blanks disallowed and default text") editorText2, err := p.MarkdownEditor("Edit your text:", defaultText, false) if err != nil { return err } - fmt.Printf("Returned text: %s\n", editorText2) + fmt.Printf("Returned text: %s\n\n", editorText2) fmt.Println("Demonstrating Markdown Editor with blanks disallowed and no default text") editorText3, err := p.MarkdownEditor("Edit your text:", "", false) From 3af32fb07026c08b6c7ba4d48f818ab1365a9e3c Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Sat, 10 May 2025 10:46:35 -0600 Subject: [PATCH 3/6] fix(preview): remove needless newlines --- pkg/cmd/preview/prompter/prompter.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/pkg/cmd/preview/prompter/prompter.go b/pkg/cmd/preview/prompter/prompter.go index 5b9b91f23..e07ec61d8 100644 --- a/pkg/cmd/preview/prompter/prompter.go +++ b/pkg/cmd/preview/prompter/prompter.go @@ -120,8 +120,6 @@ func prompterRun(opts *prompterOptions) error { if err := f(p); err != nil { return err } - // Newline for readability - fmt.Println() } return nil From c4ab455ebfbcc23b1f7e8483b6bdeb17d52525ba Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Sat, 10 May 2025 11:34:14 -0600 Subject: [PATCH 4/6] fix(prompter): update prompter create for changes in trunk --- pkg/cmd/preview/prompter/prompter.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/preview/prompter/prompter.go b/pkg/cmd/preview/prompter/prompter.go index e07ec61d8..b63ac2d04 100644 --- a/pkg/cmd/preview/prompter/prompter.go +++ b/pkg/cmd/preview/prompter/prompter.go @@ -114,7 +114,7 @@ func prompterRun(opts *prompterOptions) error { return err } - p := prompter.New(editor, opts.IO.In, opts.IO.Out, opts.IO.ErrOut) + p := prompter.New(editor, opts.IO) for _, f := range opts.PromptsToRun { if err := f(p); err != nil { From f305cb13de20cfeb205735a2a1b315358505db1c Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Sat, 10 May 2025 11:43:44 -0600 Subject: [PATCH 5/6] fix(prompter): print to iostreams stdout --- pkg/cmd/preview/prompter/prompter.go | 68 ++++++++++++++-------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/pkg/cmd/preview/prompter/prompter.go b/pkg/cmd/preview/prompter/prompter.go index b63ac2d04..5b44a5cbf 100644 --- a/pkg/cmd/preview/prompter/prompter.go +++ b/pkg/cmd/preview/prompter/prompter.go @@ -15,7 +15,7 @@ type prompterOptions struct { IO *iostreams.IOStreams Config func() (gh.Config, error) - PromptsToRun []func(prompter.Prompter) error + PromptsToRun []func(prompter.Prompter, *iostreams.IOStreams) error } func NewCmdPrompter(f *cmdutil.Factory, runF func(*prompterOptions) error) *cobra.Command { @@ -36,7 +36,7 @@ func NewCmdPrompter(f *cmdutil.Factory, runF func(*prompterOptions) error) *cobr markdownEditorPrompt = "markdown-editor" ) - prompterTypeFuncMap := map[string]func(prompter.Prompter) error{ + prompterTypeFuncMap := map[string]func(prompter.Prompter, *iostreams.IOStreams) error{ selectPrompt: runSelect, multiSelectPrompt: runMultiSelect, inputPrompt: runInput, @@ -117,7 +117,7 @@ func prompterRun(opts *prompterOptions) error { p := prompter.New(editor, opts.IO) for _, f := range opts.PromptsToRun { - if err := f(p); err != nil { + if err := f(p, opts.IO); err != nil { return err } } @@ -125,112 +125,112 @@ func prompterRun(opts *prompterOptions) error { return nil } -func runSelect(p prompter.Prompter) error { - fmt.Println("Demonstrating Single Select") +func runSelect(p prompter.Prompter, io *iostreams.IOStreams) error { + fmt.Fprintln(io.Out, "Demonstrating Single Select") cuisines := []string{"Italian", "Greek", "Indian", "Japanese", "American"} favorite, err := p.Select("Favorite cuisine?", "Italian", cuisines) if err != nil { return err } - fmt.Printf("Favorite cuisine: %s\n", cuisines[favorite]) + fmt.Fprintf(io.Out, "Favorite cuisine: %s\n", cuisines[favorite]) return nil } -func runMultiSelect(p prompter.Prompter) error { - fmt.Println("Demonstrating Multi Select") +func runMultiSelect(p prompter.Prompter, io *iostreams.IOStreams) error { + fmt.Fprintln(io.Out, "Demonstrating Multi Select") cuisines := []string{"Italian", "Greek", "Indian", "Japanese", "American"} favorites, err := p.MultiSelect("Favorite cuisines?", []string{}, cuisines) if err != nil { return err } for _, f := range favorites { - fmt.Printf("Favorite cuisine: %s\n", cuisines[f]) + fmt.Fprintf(io.Out, "Favorite cuisine: %s\n", cuisines[f]) } return nil } -func runInput(p prompter.Prompter) error { - fmt.Println("Demonstrating Text Input") +func runInput(p prompter.Prompter, io *iostreams.IOStreams) error { + fmt.Fprintln(io.Out, "Demonstrating Text Input") text, err := p.Input("Favorite meal?", "Breakfast") if err != nil { return err } - fmt.Printf("You typed: %s\n", text) + fmt.Fprintf(io.Out, "You typed: %s\n", text) return nil } -func runPassword(p prompter.Prompter) error { - fmt.Println("Demonstrating Password Input") +func runPassword(p prompter.Prompter, io *iostreams.IOStreams) error { + fmt.Fprintln(io.Out, "Demonstrating Password Input") safeword, err := p.Password("Safe word?") if err != nil { return err } - fmt.Printf("Safe word: %s\n", safeword) + fmt.Fprintf(io.Out, "Safe word: %s\n", safeword) return nil } -func runConfirm(p prompter.Prompter) error { - fmt.Println("Demonstrating Confirmation") +func runConfirm(p prompter.Prompter, io *iostreams.IOStreams) error { + fmt.Fprintln(io.Out, "Demonstrating Confirmation") confirmation, err := p.Confirm("Are you sure?", true) if err != nil { return err } - fmt.Printf("Confirmation: %t\n", confirmation) + fmt.Fprintf(io.Out, "Confirmation: %t\n", confirmation) return nil } -func runAuthToken(p prompter.Prompter) error { - fmt.Println("Demonstrating Auth Token (can't be blank)") +func runAuthToken(p prompter.Prompter, io *iostreams.IOStreams) error { + fmt.Fprintln(io.Out, "Demonstrating Auth Token (can't be blank)") token, err := p.AuthToken() if err != nil { return err } - fmt.Printf("Auth token: %s\n", token) + fmt.Fprintf(io.Out, "Auth token: %s\n", token) return nil } -func runConfirmDeletion(p prompter.Prompter) error { - fmt.Println("Demonstrating Deletion Confirmation") +func runConfirmDeletion(p prompter.Prompter, io *iostreams.IOStreams) error { + fmt.Fprintln(io.Out, "Demonstrating Deletion Confirmation") err := p.ConfirmDeletion("delete-me") if err != nil { return err } - fmt.Println("Item deleted") + fmt.Fprintln(io.Out, "Item deleted") return nil } -func runInputHostname(p prompter.Prompter) error { - fmt.Println("Demonstrating Hostname") +func runInputHostname(p prompter.Prompter, io *iostreams.IOStreams) error { + fmt.Fprintln(io.Out, "Demonstrating Hostname") hostname, err := p.InputHostname() if err != nil { return err } - fmt.Printf("Hostname: %s\n", hostname) + fmt.Fprintf(io.Out, "Hostname: %s\n", hostname) return nil } -func runMarkdownEditor(p prompter.Prompter) error { +func runMarkdownEditor(p prompter.Prompter, io *iostreams.IOStreams) error { defaultText := "default text value" - fmt.Println("Demonstrating Markdown Editor with blanks allowed and default text") + fmt.Fprintln(io.Out, "Demonstrating Markdown Editor with blanks allowed and default text") editorText, err := p.MarkdownEditor("Edit your text:", defaultText, true) if err != nil { return err } - fmt.Printf("Returned text: %s\n\n", editorText) + fmt.Fprintf(io.Out, "Returned text: %s\n\n", editorText) - fmt.Println("Demonstrating Markdown Editor with blanks disallowed and default text") + fmt.Fprintln(io.Out, "Demonstrating Markdown Editor with blanks disallowed and default text") editorText2, err := p.MarkdownEditor("Edit your text:", defaultText, false) if err != nil { return err } - fmt.Printf("Returned text: %s\n\n", editorText2) + fmt.Fprintf(io.Out, "Returned text: %s\n\n", editorText2) - fmt.Println("Demonstrating Markdown Editor with blanks disallowed and no default text") + fmt.Fprintln(io.Out, "Demonstrating Markdown Editor with blanks disallowed and no default text") editorText3, err := p.MarkdownEditor("Edit your text:", "", false) if err != nil { return err } - fmt.Printf("Returned text: %s\n", editorText3) + fmt.Fprintf(io.Out, "Returned text: %s\n", editorText3) return nil } From 175767cc59db46e2ac235859af6c12a57248bf58 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Sat, 10 May 2025 11:54:32 -0600 Subject: [PATCH 6/6] doc(preview): add long description --- pkg/cmd/preview/preview.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/cmd/preview/preview.go b/pkg/cmd/preview/preview.go index 5c3209722..791c100b6 100644 --- a/pkg/cmd/preview/preview.go +++ b/pkg/cmd/preview/preview.go @@ -1,6 +1,7 @@ package preview import ( + "github.com/MakeNowJust/heredoc" cmdPrompter "github.com/cli/cli/v2/pkg/cmd/preview/prompter" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/spf13/cobra" @@ -10,6 +11,10 @@ func NewCmdPreview(f *cmdutil.Factory) *cobra.Command { cmd := &cobra.Command{ Use: "preview ", Short: "Execute previews for gh features", + Long: heredoc.Doc(` + Preview commands are for testing, demonstrative, and development purposes only. + They should be considered unstable and can change at any time. + `), } cmdutil.DisableAuthCheck(cmd)