Skip to content

Commit

Permalink
Solidify editor mechanics (#227)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
cyx authored Apr 2, 2021
1 parent ce04d9b commit 2220ac6
Show file tree
Hide file tree
Showing 2 changed files with 84 additions and 49 deletions.
24 changes: 15 additions & 9 deletions internal/cli/rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand All @@ -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)
Expand Down Expand Up @@ -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 <id>`,
PreRun: func(cmd *cobra.Command, args []string) {
Expand Down Expand Up @@ -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 <rule-id>`,
PreRun: func(cmd *cobra.Command, args []string) {
Expand Down Expand Up @@ -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 <rule-id>
auth0 rules update <rule-id> --name "My Updated Rule"
auth0 rules update <rule-id> -n "My Updated Rule" --enabled=false`,
Expand Down Expand Up @@ -291,6 +294,9 @@ auth0 rules update <rule-id> -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)
Expand Down
109 changes: 69 additions & 40 deletions internal/prompt/editor.go
Original file line number Diff line number Diff line change
@@ -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}
cliEditors = []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()
}

Expand All @@ -72,35 +74,62 @@ 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
}

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
}

0 comments on commit 2220ac6

Please sign in to comment.