From 2bd15195e599daaf47d8fdb51f31c967382d0b22 Mon Sep 17 00:00:00 2001 From: Cyril David Date: Fri, 2 Apr 2021 11:48:40 -0700 Subject: [PATCH] Solidify editor mechanics I've looked into what github does, so they use a mix of this approach, and the survey approach. To keep it simple, we're only gonna go with the manual approach they do until we feel the full blown prompt approach is necessary. Closes #189 --- internal/cli/rules.go | 24 +++++---- internal/prompt/editor.go | 109 ++++++++++++++++++++++++-------------- 2 files changed, 84 insertions(+), 49 deletions(-) diff --git a/internal/cli/rules.go b/internal/cli/rules.go index 6f03c0ddd..fac10b1fd 100644 --- a/internal/cli/rules.go +++ b/internal/cli/rules.go @@ -67,10 +67,10 @@ func rulesCmd(cli *cli) *cobra.Command { func listRulesCmd(cli *cli) *cobra.Command { cmd := &cobra.Command{ - Use: "list", + Use: "list", Aliases: []string{"ls"}, - Short: "List your rules", - Long: `List the rules in your current tenant.`, + Short: "List your rules", + Long: `List the rules in your current tenant.`, Example: `auth0 rules list auth0 rules ls`, RunE: func(cmd *cobra.Command, args []string) error { @@ -106,9 +106,9 @@ func createRuleCmd(cli *cli) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Create a new rule", - Long: `Create a new rule:`, - Example: `auth0 rules create -auth0 rules create --name "My Rule" + Long: `Create a new rule:`, + Example: `auth0 rules create +auth0 rules create --name "My Rule" auth0 rules create -n "My Rule" --template "Empty rule" auth0 rules create -n "My Rule" -t "Empty rule" --enabled=false`, PreRun: func(cmd *cobra.Command, args []string) { @@ -129,6 +129,9 @@ auth0 rules create -n "My Rule" -t "Empty rule" --enabled=false`, script, err := prompt.CaptureInputViaEditor( ruleTemplateOptions.getValue(inputs.Template), inputs.Name+".*.js", + func() { + cli.renderer.Infof("%s once you close the editor, the rule will be saved. To cancel, CTRL+C.", ansi.Faint("Hint:")) + }, ) if err != nil { return fmt.Errorf("Failed to capture input from the editor: %w", err) @@ -169,7 +172,7 @@ func showRuleCmd(cli *cli) *cobra.Command { Use: "show", Args: cobra.MaximumNArgs(1), Short: "Show a rule", - Long: `Show a rule:`, + Long: `Show a rule:`, Example: `auth0 rules show auth0 rules show `, PreRun: func(cmd *cobra.Command, args []string) { @@ -213,7 +216,7 @@ func deleteRuleCmd(cli *cli) *cobra.Command { cmd := &cobra.Command{ Use: "delete", Short: "Delete a rule", - Long: `Delete a rule`, + Long: `Delete a rule`, Example: `auth0 rules delete auth0 rules delete `, PreRun: func(cmd *cobra.Command, args []string) { @@ -254,7 +257,7 @@ func updateRuleCmd(cli *cli) *cobra.Command { cmd := &cobra.Command{ Use: "update", Short: "Update a rule", - Long: `Update a rule`, + Long: `Update a rule`, Example: `auth0 rules update auth0 rules update --name "My Updated Rule" auth0 rules update -n "My Updated Rule" --enabled=false`, @@ -291,6 +294,9 @@ auth0 rules update -n "My Updated Rule" --enabled=false`, script, err := prompt.CaptureInputViaEditor( rule.GetScript(), rule.GetName()+".*.js", + func() { + cli.renderer.Infof("%s once you close the editor, the rule will be saved. To cancel, CTRL+C.", ansi.Faint("Hint:")) + }, ) if err != nil { return fmt.Errorf("Failed to capture input from the editor: %w", err) diff --git a/internal/prompt/editor.go b/internal/prompt/editor.go index a063c0969..10b9f86fe 100644 --- a/internal/prompt/editor.go +++ b/internal/prompt/editor.go @@ -1,68 +1,70 @@ package prompt import ( + "bytes" "fmt" "os" "os/exec" - "strings" + "runtime" + + "github.com/kballard/go-shellquote" ) -const defaultEditor = "vim" +const ( + defaultEditor = "vim" +) -var defaultEditorPrompt = &editorPrompt{defaultEditor: defaultEditor} +var ( + bom = []byte{0xef, 0xbb, 0xbf} + editorCmdcliEditors = []string{"emacs", "micro", "nano", "nvim", "vi", "vim", "nvim"} +) + +var defaultEditorPrompt = &editorPrompt{cmd: getDefaultEditor()} // CaptureInputViaEditor is the high level function to use in this package in // order to capture input from an editor. // // The arguments have been tailored for our use of strings mostly in the rest // of the CLI even though internally we're using []byte. -func CaptureInputViaEditor(contents, pattern string) (result string, err error) { - v, err := defaultEditorPrompt.captureInput([]byte(contents), pattern) +func CaptureInputViaEditor(contents, pattern string, infoFn func()) (result string, err error) { + v, err := defaultEditorPrompt.captureInput([]byte(contents), pattern, infoFn) return string(v), err } type editorPrompt struct { - defaultEditor string + cmd string } -// GetPreferredEditorFromEnvironment returns the user's editor as defined by the -// `$EDITOR` environment variable, or the `defaultEditor` if it is not set. -func (p *editorPrompt) getPreferredEditor() string { - editor := os.Getenv("EDITOR") - - if editor == "" { - return p.defaultEditor +// openFile opens filename in the preferred text editor, resolving the +// arguments with editor specific logic. +func (p *editorPrompt) openFile(filename string, infoFn func()) error { + args, err := shellquote.Split(p.cmd) + if err != nil { + return err } + args = append(args, filename) - return editor -} - -func (p *editorPrompt) resolveEditorArguments(executable string, filename string) []string { - args := []string{filename} - - if strings.Contains(executable, "Visual Studio Code.app") { - args = append([]string{"--wait"}, args...) + isCLIEditor := false + for _, e := range cliEditors { + if e == args[0] { + isCLIEditor = true + } } - // TODO(cyx): add other common editors - - return args -} - -// openFile opens filename in the preferred text editor, resolving the -// arguments with editor specific logic. -func (p *editorPrompt) openFile(filename string) error { - // Get the full executable path for the editor. - executable, err := exec.LookPath(p.getPreferredEditor()) + editorExe, err := exec.LookPath(args[0]) if err != nil { return err } - cmd := exec.Command(executable, p.resolveEditorArguments(executable, filename)...) + cmd := exec.Command(editorExe, args[1:]...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr + if !isCLIEditor && infoFn != nil { + infoFn() + } + return cmd.Run() } @@ -72,7 +74,7 @@ func (p *editorPrompt) openFile(filename string) error { // // If given default contents, it will write that to the file before popping // open the editor. -func (p *editorPrompt) captureInput(contents []byte, pattern string) ([]byte, error) { +func (p *editorPrompt) captureInput(contents []byte, pattern string, infoFn func()) ([]byte, error) { file, err := os.CreateTemp(os.TempDir(), pattern) if err != nil { return []byte{}, err @@ -80,27 +82,54 @@ func (p *editorPrompt) captureInput(contents []byte, pattern string) ([]byte, er filename := file.Name() + // Defer removal of the temporary file in case any of the next steps fail. + defer os.Remove(filename) + + // write utf8 BOM header + // The reason why we do this is because notepad.exe on Windows determines the + // encoding of an "empty" text file by the locale, for example, GBK in China, + // while golang string only handles utf8 well. However, a text file with utf8 + // BOM header is not considered "empty" on Windows, and the encoding will then + // be determined utf8 by notepad.exe, instead of GBK or other encodings. + if _, err := file.Write(bom); err != nil { + return nil, err + } + if len(contents) > 0 { - if err := os.WriteFile(filename, contents, 0644); err != nil { + if _, err := file.Write(contents); err != nil { return nil, fmt.Errorf("Failed to write to file: %w", err) } } - // Defer removal of the temporary file in case any of the next steps fail. - defer os.Remove(filename) - if err = file.Close(); err != nil { return nil, err } - if err = p.openFile(filename); err != nil { + if err = p.openFile(filename, infoFn); err != nil { return nil, err } - bytes, err := os.ReadFile(filename) + raw, err := os.ReadFile(filename) if err != nil { return []byte{}, err } - return bytes, nil + // strip BOM header + return bytes.TrimPrefix(raw, bom), nil +} + +// getDefaultEditor is taken from https://github.com/cli/cli/blob/trunk/pkg/surveyext/editor_manual.go +// and tries to infer the editor from different heuristics. +func getDefaultEditor() string { + if runtime.GOOS == "windows" { + return "notepad" + } else if g := os.Getenv("GIT_EDITOR"); g != "" { + return g + } else if v := os.Getenv("VISUAL"); v != "" { + return v + } else if e := os.Getenv("EDITOR"); e != "" { + return e + } + + return defaultEditor }