pkg/surveyext/surveyext.go (187 lines of code) (raw):

// this is a wrapper for https://github.com/AlecAivazis/survey package but with // additional extensions and customizations for glab // adapted from https://github.com/cli/cli/blob/trunk/pkg/surveyext/editor.go package surveyext import ( "bytes" "io" "os" "os/exec" "path/filepath" "runtime" "github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2/terminal" "github.com/kballard/go-shellquote" "gitlab.com/gitlab-org/cli/pkg/execext" ) var ( bom = []byte{0xef, 0xbb, 0xbf} defaultEditor = "nano" // EXTENDED to switch from vim as a default editor editorSkipKey = 's' editorOpenKey = 'e' ) type showable interface { Show() error } func init() { if runtime.GOOS == "windows" { defaultEditor = "notepad" } else if g := os.Getenv("GIT_EDITOR"); g != "" { defaultEditor = g } else if v := os.Getenv("VISUAL"); v != "" { defaultEditor = v } else if e := os.Getenv("EDITOR"); e != "" { defaultEditor = e } } // EXTENDED to enable different prompting behavior type GLabEditor struct { *survey.Editor EditorCommand string BlankAllowed bool } func (e *GLabEditor) editorCommand() string { if e.EditorCommand == "" { return defaultEditor } return e.EditorCommand } // EXTENDED to change prompt text var EditorQuestionTemplate = ` {{- if .ShowHelp }}{{- color .Config.Icons.Help.Format }}{{ .Config.Icons.Help.Text }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}} {{- color .Config.Icons.Question.Format }}{{ .Config.Icons.Question.Text }} {{color "reset"}} {{- color "default+hb"}}{{ .Message }} {{color "reset"}} {{- if .ShowAnswer}} {{- color "cyan"}}{{.Answer}}{{color "reset"}}{{"\n"}} {{- else }} {{- if and .Help (not .ShowHelp)}}{{color "cyan"}}[{{ .Config.HelpInput }} for help]{{color "reset"}} {{end}} {{- if and .Default (not .HideDefault)}}{{color "white"}}({{.Default}}) {{color "reset"}}{{end}} {{- color "cyan"}}[(e) or Enter to launch {{ .EditorCommand }}{{- if .BlankAllowed }}, (s) or Esc to skip{{ end }}] {{color "reset"}} {{- end}}` // EXTENDED to pass editor name (to use in prompt) type EditorTemplateData struct { survey.Editor EditorCommand string BlankAllowed bool Answer string ShowAnswer bool ShowHelp bool Config *survey.PromptConfig } // EXTENDED to augment prompt text and keypress handling func (e *GLabEditor) prompt(initialValue string, config *survey.PromptConfig) (any, error) { err := e.Render( EditorQuestionTemplate, // EXTENDED to support printing editor in prompt and BlankAllowed EditorTemplateData{ Editor: *e.Editor, BlankAllowed: e.BlankAllowed, EditorCommand: filepath.Base(e.editorCommand()), Config: config, }, ) if err != nil { return "", err } // start reading runes from the standard in rr := e.NewRuneReader() _ = rr.SetTermMode() defer func() { _ = rr.RestoreTermMode() }() cursor := e.NewCursor() _ = cursor.Hide() defer func() { _ = cursor.Show() }() for { // EXTENDED to handle the Enter or e to edit / s or Esc to skip behavior + BlankAllowed r, _, err := rr.ReadRune() if err != nil { return "", err } if r == editorOpenKey || r == '\r' || r == '\n' { break } if r == editorSkipKey || r == terminal.KeyEscape { if e.BlankAllowed { return "", nil } else { continue } } if r == terminal.KeyInterrupt { return "", terminal.InterruptErr } if r == terminal.KeyEndTransmission { break } if string(r) == config.HelpInput && e.Help != "" { err = e.Render( EditorQuestionTemplate, EditorTemplateData{ // EXTENDED to support printing editor in prompt, BlankAllowed Editor: *e.Editor, BlankAllowed: e.BlankAllowed, EditorCommand: filepath.Base(e.editorCommand()), ShowHelp: true, Config: config, }, ) if err != nil { return "", err } } continue } stdio := e.Stdio() text, err := Edit(e.editorCommand(), e.FileName, initialValue, stdio.In, stdio.Out, stdio.Err, cursor) if err != nil { return "", err } // check length, return default value on empty if text == "" && !e.AppendDefault { return e.Default, nil } return text, nil } // EXTENDED This is straight copypasta from survey to get our overridden prompt called.; func (e *GLabEditor) Prompt(config *survey.PromptConfig) (any, error) { initialValue := "" if e.Default != "" && e.AppendDefault { initialValue = e.Default } return e.prompt(initialValue, config) } func Edit(editorCommand, fn, initialValue string, stdin io.Reader, stdout io.Writer, stderr io.Writer, cursor showable) (string, error) { // prepare the temp file pattern := fn if pattern == "" { pattern = "survey*.txt" } f, err := os.CreateTemp("", pattern) if err != nil { return "", err } defer os.Remove(f.Name()) // 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 := f.Write(bom); err != nil { return "", err } // write initial value if _, err := f.WriteString(initialValue); err != nil { return "", err } // close the fd to prevent the editor unable to save file if err := f.Close(); err != nil { return "", err } if editorCommand == "" { editorCommand = defaultEditor } args, err := shellquote.Split(editorCommand) if err != nil { return "", err } args = append(args, f.Name()) editorExe, err := execext.LookPath(args[0]) if err != nil { return "", err } cmd := exec.Command(editorExe, args[1:]...) cmd.Stdin = stdin cmd.Stdout = stdout cmd.Stderr = stderr if cursor != nil { _ = cursor.Show() } // open the editor if err := cmd.Run(); err != nil { return "", err } // raw is a BOM-unstripped UTF8 byte slice raw, err := os.ReadFile(f.Name()) if err != nil { return "", err } // strip BOM header return string(bytes.TrimPrefix(raw, bom)), nil }