editor.go 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. package survey
  2. import (
  3. "bytes"
  4. "io/ioutil"
  5. "os"
  6. "os/exec"
  7. "runtime"
  8. shellquote "github.com/kballard/go-shellquote"
  9. "gopkg.in/AlecAivazis/survey.v1/core"
  10. "gopkg.in/AlecAivazis/survey.v1/terminal"
  11. )
  12. /*
  13. Editor launches an instance of the users preferred editor on a temporary file.
  14. The editor to use is determined by reading the $VISUAL or $EDITOR environment
  15. variables. If neither of those are present, notepad (on Windows) or vim
  16. (others) is used.
  17. The launch of the editor is triggered by the enter key. Since the response may
  18. be long, it will not be echoed as Input does, instead, it print <Received>.
  19. Response type is a string.
  20. message := ""
  21. prompt := &survey.Editor{ Message: "What is your commit message?" }
  22. survey.AskOne(prompt, &message, nil)
  23. */
  24. type Editor struct {
  25. core.Renderer
  26. Message string
  27. Default string
  28. Help string
  29. Editor string
  30. HideDefault bool
  31. AppendDefault bool
  32. }
  33. // data available to the templates when processing
  34. type EditorTemplateData struct {
  35. Editor
  36. Answer string
  37. ShowAnswer bool
  38. ShowHelp bool
  39. }
  40. // Templates with Color formatting. See Documentation: https://github.com/mgutz/ansi#style-format
  41. var EditorQuestionTemplate = `
  42. {{- if .ShowHelp }}{{- color "cyan"}}{{ HelpIcon }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}}
  43. {{- color "green+hb"}}{{ QuestionIcon }} {{color "reset"}}
  44. {{- color "default+hb"}}{{ .Message }} {{color "reset"}}
  45. {{- if .ShowAnswer}}
  46. {{- color "cyan"}}{{.Answer}}{{color "reset"}}{{"\n"}}
  47. {{- else }}
  48. {{- if and .Help (not .ShowHelp)}}{{color "cyan"}}[{{ HelpInputRune }} for help]{{color "reset"}} {{end}}
  49. {{- if and .Default (not .HideDefault)}}{{color "white"}}({{.Default}}) {{color "reset"}}{{end}}
  50. {{- color "cyan"}}[Enter to launch editor] {{color "reset"}}
  51. {{- end}}`
  52. var (
  53. bom = []byte{0xef, 0xbb, 0xbf}
  54. editor = "vim"
  55. )
  56. func init() {
  57. if runtime.GOOS == "windows" {
  58. editor = "notepad"
  59. }
  60. if v := os.Getenv("VISUAL"); v != "" {
  61. editor = v
  62. } else if e := os.Getenv("EDITOR"); e != "" {
  63. editor = e
  64. }
  65. }
  66. func (e *Editor) Prompt() (interface{}, error) {
  67. // render the template
  68. err := e.Render(
  69. EditorQuestionTemplate,
  70. EditorTemplateData{Editor: *e},
  71. )
  72. if err != nil {
  73. return "", err
  74. }
  75. // start reading runes from the standard in
  76. rr := e.NewRuneReader()
  77. rr.SetTermMode()
  78. defer rr.RestoreTermMode()
  79. cursor := e.NewCursor()
  80. cursor.Hide()
  81. defer cursor.Show()
  82. for {
  83. r, _, err := rr.ReadRune()
  84. if err != nil {
  85. return "", err
  86. }
  87. if r == '\r' || r == '\n' {
  88. break
  89. }
  90. if r == terminal.KeyInterrupt {
  91. return "", terminal.InterruptErr
  92. }
  93. if r == terminal.KeyEndTransmission {
  94. break
  95. }
  96. if r == core.HelpInputRune && e.Help != "" {
  97. err = e.Render(
  98. EditorQuestionTemplate,
  99. EditorTemplateData{Editor: *e, ShowHelp: true},
  100. )
  101. if err != nil {
  102. return "", err
  103. }
  104. }
  105. continue
  106. }
  107. // prepare the temp file
  108. f, err := ioutil.TempFile("", "survey")
  109. if err != nil {
  110. return "", err
  111. }
  112. defer os.Remove(f.Name())
  113. // write utf8 BOM header
  114. // The reason why we do this is because notepad.exe on Windows determines the
  115. // encoding of an "empty" text file by the locale, for example, GBK in China,
  116. // while golang string only handles utf8 well. However, a text file with utf8
  117. // BOM header is not considered "empty" on Windows, and the encoding will then
  118. // be determined utf8 by notepad.exe, instead of GBK or other encodings.
  119. if _, err := f.Write(bom); err != nil {
  120. return "", err
  121. }
  122. // write default value
  123. if e.Default != "" && e.AppendDefault {
  124. if _, err := f.WriteString(e.Default); err != nil {
  125. return "", err
  126. }
  127. }
  128. // close the fd to prevent the editor unable to save file
  129. if err := f.Close(); err != nil {
  130. return "", err
  131. }
  132. // check is input editor exist
  133. if e.Editor != "" {
  134. editor = e.Editor
  135. }
  136. stdio := e.Stdio()
  137. args, err := shellquote.Split(editor)
  138. if err != nil {
  139. return "", err
  140. }
  141. args = append(args, f.Name())
  142. // open the editor
  143. cmd := exec.Command(args[0], args[1:]...)
  144. cmd.Stdin = stdio.In
  145. cmd.Stdout = stdio.Out
  146. cmd.Stderr = stdio.Err
  147. cursor.Show()
  148. if err := cmd.Run(); err != nil {
  149. return "", err
  150. }
  151. // raw is a BOM-unstripped UTF8 byte slice
  152. raw, err := ioutil.ReadFile(f.Name())
  153. if err != nil {
  154. return "", err
  155. }
  156. // strip BOM header
  157. text := string(bytes.TrimPrefix(raw, bom))
  158. // check length, return default value on empty
  159. if len(text) == 0 && !e.AppendDefault {
  160. return e.Default, nil
  161. }
  162. return text, nil
  163. }
  164. func (e *Editor) Cleanup(val interface{}) error {
  165. return e.Render(
  166. EditorQuestionTemplate,
  167. EditorTemplateData{Editor: *e, Answer: "<Received>", ShowAnswer: true},
  168. )
  169. }