forked from toolshed/abra
		
	
		
			
				
	
	
		
			227 lines
		
	
	
		
			5.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			227 lines
		
	
	
		
			5.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package survey
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"io/ioutil"
 | |
| 	"os"
 | |
| 	"os/exec"
 | |
| 	"runtime"
 | |
| 
 | |
| 	"github.com/AlecAivazis/survey/v2/terminal"
 | |
| 	shellquote "github.com/kballard/go-shellquote"
 | |
| )
 | |
| 
 | |
| /*
 | |
| Editor launches an instance of the users preferred editor on a temporary file.
 | |
| The editor to use is determined by reading the $VISUAL or $EDITOR environment
 | |
| variables. If neither of those are present, notepad (on Windows) or vim
 | |
| (others) is used.
 | |
| The launch of the editor is triggered by the enter key. Since the response may
 | |
| be long, it will not be echoed as Input does, instead, it print <Received>.
 | |
| Response type is a string.
 | |
| 
 | |
| 	message := ""
 | |
| 	prompt := &survey.Editor{ Message: "What is your commit message?" }
 | |
| 	survey.AskOne(prompt, &message)
 | |
| */
 | |
| type Editor struct {
 | |
| 	Renderer
 | |
| 	Message       string
 | |
| 	Default       string
 | |
| 	Help          string
 | |
| 	Editor        string
 | |
| 	HideDefault   bool
 | |
| 	AppendDefault bool
 | |
| 	FileName      string
 | |
| }
 | |
| 
 | |
| // data available to the templates when processing
 | |
| type EditorTemplateData struct {
 | |
| 	Editor
 | |
| 	Answer     string
 | |
| 	ShowAnswer bool
 | |
| 	ShowHelp   bool
 | |
| 	Config     *PromptConfig
 | |
| }
 | |
| 
 | |
| // Templates with Color formatting. See Documentation: https://github.com/mgutz/ansi#style-format
 | |
| 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"}}[Enter to launch editor] {{color "reset"}}
 | |
| {{- end}}`
 | |
| 
 | |
| var (
 | |
| 	bom    = []byte{0xef, 0xbb, 0xbf}
 | |
| 	editor = "vim"
 | |
| )
 | |
| 
 | |
| func init() {
 | |
| 	if runtime.GOOS == "windows" {
 | |
| 		editor = "notepad"
 | |
| 	}
 | |
| 	if v := os.Getenv("VISUAL"); v != "" {
 | |
| 		editor = v
 | |
| 	} else if e := os.Getenv("EDITOR"); e != "" {
 | |
| 		editor = e
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (e *Editor) PromptAgain(config *PromptConfig, invalid interface{}, err error) (interface{}, error) {
 | |
| 	initialValue := invalid.(string)
 | |
| 	return e.prompt(initialValue, config)
 | |
| }
 | |
| 
 | |
| func (e *Editor) Prompt(config *PromptConfig) (interface{}, error) {
 | |
| 	initialValue := ""
 | |
| 	if e.Default != "" && e.AppendDefault {
 | |
| 		initialValue = e.Default
 | |
| 	}
 | |
| 	return e.prompt(initialValue, config)
 | |
| }
 | |
| 
 | |
| func (e *Editor) prompt(initialValue string, config *PromptConfig) (interface{}, error) {
 | |
| 	// render the template
 | |
| 	err := e.Render(
 | |
| 		EditorQuestionTemplate,
 | |
| 		EditorTemplateData{
 | |
| 			Editor: *e,
 | |
| 			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 cursor.Show()
 | |
| 
 | |
| 	for {
 | |
| 		r, _, err := rr.ReadRune()
 | |
| 		if err != nil {
 | |
| 			return "", err
 | |
| 		}
 | |
| 		if r == '\r' || r == '\n' {
 | |
| 			break
 | |
| 		}
 | |
| 		if r == terminal.KeyInterrupt {
 | |
| 			return "", terminal.InterruptErr
 | |
| 		}
 | |
| 		if r == terminal.KeyEndTransmission {
 | |
| 			break
 | |
| 		}
 | |
| 		if string(r) == config.HelpInput && e.Help != "" {
 | |
| 			err = e.Render(
 | |
| 				EditorQuestionTemplate,
 | |
| 				EditorTemplateData{
 | |
| 					Editor:   *e,
 | |
| 					ShowHelp: true,
 | |
| 					Config:   config,
 | |
| 				},
 | |
| 			)
 | |
| 			if err != nil {
 | |
| 				return "", err
 | |
| 			}
 | |
| 		}
 | |
| 		continue
 | |
| 	}
 | |
| 
 | |
| 	// prepare the temp file
 | |
| 	pattern := e.FileName
 | |
| 	if pattern == "" {
 | |
| 		pattern = "survey*.txt"
 | |
| 	}
 | |
| 	f, err := ioutil.TempFile("", pattern)
 | |
| 	if err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 	defer func() {
 | |
| 		_ = 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
 | |
| 	}
 | |
| 
 | |
| 	// check is input editor exist
 | |
| 	if e.Editor != "" {
 | |
| 		editor = e.Editor
 | |
| 	}
 | |
| 
 | |
| 	stdio := e.Stdio()
 | |
| 
 | |
| 	args, err := shellquote.Split(editor)
 | |
| 	if err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 	args = append(args, f.Name())
 | |
| 
 | |
| 	// open the editor
 | |
| 	cmd := exec.Command(args[0], args[1:]...)
 | |
| 	cmd.Stdin = stdio.In
 | |
| 	cmd.Stdout = stdio.Out
 | |
| 	cmd.Stderr = stdio.Err
 | |
| 	cursor.Show()
 | |
| 	if err := cmd.Run(); err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 
 | |
| 	// raw is a BOM-unstripped UTF8 byte slice
 | |
| 	raw, err := ioutil.ReadFile(f.Name())
 | |
| 	if err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 
 | |
| 	// strip BOM header
 | |
| 	text := string(bytes.TrimPrefix(raw, bom))
 | |
| 
 | |
| 	// check length, return default value on empty
 | |
| 	if len(text) == 0 && !e.AppendDefault {
 | |
| 		return e.Default, nil
 | |
| 	}
 | |
| 
 | |
| 	return text, nil
 | |
| }
 | |
| 
 | |
| func (e *Editor) Cleanup(config *PromptConfig, val interface{}) error {
 | |
| 	return e.Render(
 | |
| 		EditorQuestionTemplate,
 | |
| 		EditorTemplateData{
 | |
| 			Editor:     *e,
 | |
| 			Answer:     "<Received>",
 | |
| 			ShowAnswer: true,
 | |
| 			Config:     config,
 | |
| 		},
 | |
| 	)
 | |
| }
 |