common.go 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. /*
  2. Package liner implements a simple command line editor, inspired by linenoise
  3. (https://github.com/antirez/linenoise/). This package supports WIN32 in
  4. addition to the xterm codes supported by everything else.
  5. */
  6. package liner
  7. import (
  8. "bufio"
  9. "container/ring"
  10. "errors"
  11. "fmt"
  12. "io"
  13. "strings"
  14. "sync"
  15. "unicode/utf8"
  16. )
  17. type commonState struct {
  18. terminalSupported bool
  19. outputRedirected bool
  20. inputRedirected bool
  21. history []string
  22. historyMutex sync.RWMutex
  23. completer WordCompleter
  24. columns int
  25. killRing *ring.Ring
  26. ctrlCAborts bool
  27. r *bufio.Reader
  28. tabStyle TabStyle
  29. multiLineMode bool
  30. cursorRows int
  31. maxRows int
  32. shouldRestart ShouldRestart
  33. needRefresh bool
  34. }
  35. // TabStyle is used to select how tab completions are displayed.
  36. type TabStyle int
  37. // Two tab styles are currently available:
  38. //
  39. // TabCircular cycles through each completion item and displays it directly on
  40. // the prompt
  41. //
  42. // TabPrints prints the list of completion items to the screen after a second
  43. // tab key is pressed. This behaves similar to GNU readline and BASH (which
  44. // uses readline)
  45. const (
  46. TabCircular TabStyle = iota
  47. TabPrints
  48. )
  49. // ErrPromptAborted is returned from Prompt or PasswordPrompt when the user presses Ctrl-C
  50. // if SetCtrlCAborts(true) has been called on the State
  51. var ErrPromptAborted = errors.New("prompt aborted")
  52. // ErrNotTerminalOutput is returned from Prompt or PasswordPrompt if the
  53. // platform is normally supported, but stdout has been redirected
  54. var ErrNotTerminalOutput = errors.New("standard output is not a terminal")
  55. // ErrInvalidPrompt is returned from Prompt or PasswordPrompt if the
  56. // prompt contains any unprintable runes (including substrings that could
  57. // be colour codes on some platforms).
  58. var ErrInvalidPrompt = errors.New("invalid prompt")
  59. // KillRingMax is the max number of elements to save on the killring.
  60. const KillRingMax = 60
  61. // HistoryLimit is the maximum number of entries saved in the scrollback history.
  62. const HistoryLimit = 1000
  63. // ReadHistory reads scrollback history from r. Returns the number of lines
  64. // read, and any read error (except io.EOF).
  65. func (s *State) ReadHistory(r io.Reader) (num int, err error) {
  66. s.historyMutex.Lock()
  67. defer s.historyMutex.Unlock()
  68. in := bufio.NewReader(r)
  69. num = 0
  70. for {
  71. line, part, err := in.ReadLine()
  72. if err == io.EOF {
  73. break
  74. }
  75. if err != nil {
  76. return num, err
  77. }
  78. if part {
  79. return num, fmt.Errorf("line %d is too long", num+1)
  80. }
  81. if !utf8.Valid(line) {
  82. return num, fmt.Errorf("invalid string at line %d", num+1)
  83. }
  84. num++
  85. s.history = append(s.history, string(line))
  86. if len(s.history) > HistoryLimit {
  87. s.history = s.history[1:]
  88. }
  89. }
  90. return num, nil
  91. }
  92. // WriteHistory writes scrollback history to w. Returns the number of lines
  93. // successfully written, and any write error.
  94. //
  95. // Unlike the rest of liner's API, WriteHistory is safe to call
  96. // from another goroutine while Prompt is in progress.
  97. // This exception is to facilitate the saving of the history buffer
  98. // during an unexpected exit (for example, due to Ctrl-C being invoked)
  99. func (s *State) WriteHistory(w io.Writer) (num int, err error) {
  100. s.historyMutex.RLock()
  101. defer s.historyMutex.RUnlock()
  102. for _, item := range s.history {
  103. _, err := fmt.Fprintln(w, item)
  104. if err != nil {
  105. return num, err
  106. }
  107. num++
  108. }
  109. return num, nil
  110. }
  111. // AppendHistory appends an entry to the scrollback history. AppendHistory
  112. // should be called iff Prompt returns a valid command.
  113. func (s *State) AppendHistory(item string) {
  114. s.historyMutex.Lock()
  115. defer s.historyMutex.Unlock()
  116. if len(s.history) > 0 {
  117. if item == s.history[len(s.history)-1] {
  118. return
  119. }
  120. }
  121. s.history = append(s.history, item)
  122. if len(s.history) > HistoryLimit {
  123. s.history = s.history[1:]
  124. }
  125. }
  126. // ClearHistory clears the scroollback history.
  127. func (s *State) ClearHistory() {
  128. s.historyMutex.Lock()
  129. defer s.historyMutex.Unlock()
  130. s.history = nil
  131. }
  132. // Returns the history lines starting with prefix
  133. func (s *State) getHistoryByPrefix(prefix string) (ph []string) {
  134. for _, h := range s.history {
  135. if strings.HasPrefix(h, prefix) {
  136. ph = append(ph, h)
  137. }
  138. }
  139. return
  140. }
  141. // Returns the history lines matching the inteligent search
  142. func (s *State) getHistoryByPattern(pattern string) (ph []string, pos []int) {
  143. if pattern == "" {
  144. return
  145. }
  146. for _, h := range s.history {
  147. if i := strings.Index(h, pattern); i >= 0 {
  148. ph = append(ph, h)
  149. pos = append(pos, i)
  150. }
  151. }
  152. return
  153. }
  154. // Completer takes the currently edited line content at the left of the cursor
  155. // and returns a list of completion candidates.
  156. // If the line is "Hello, wo!!!" and the cursor is before the first '!', "Hello, wo" is passed
  157. // to the completer which may return {"Hello, world", "Hello, Word"} to have "Hello, world!!!".
  158. type Completer func(line string) []string
  159. // WordCompleter takes the currently edited line with the cursor position and
  160. // returns the completion candidates for the partial word to be completed.
  161. // If the line is "Hello, wo!!!" and the cursor is before the first '!', ("Hello, wo!!!", 9) is passed
  162. // to the completer which may returns ("Hello, ", {"world", "Word"}, "!!!") to have "Hello, world!!!".
  163. type WordCompleter func(line string, pos int) (head string, completions []string, tail string)
  164. // SetCompleter sets the completion function that Liner will call to
  165. // fetch completion candidates when the user presses tab.
  166. func (s *State) SetCompleter(f Completer) {
  167. if f == nil {
  168. s.completer = nil
  169. return
  170. }
  171. s.completer = func(line string, pos int) (string, []string, string) {
  172. return "", f(string([]rune(line)[:pos])), string([]rune(line)[pos:])
  173. }
  174. }
  175. // SetWordCompleter sets the completion function that Liner will call to
  176. // fetch completion candidates when the user presses tab.
  177. func (s *State) SetWordCompleter(f WordCompleter) {
  178. s.completer = f
  179. }
  180. // SetTabCompletionStyle sets the behvavior when the Tab key is pressed
  181. // for auto-completion. TabCircular is the default behavior and cycles
  182. // through the list of candidates at the prompt. TabPrints will print
  183. // the available completion candidates to the screen similar to BASH
  184. // and GNU Readline
  185. func (s *State) SetTabCompletionStyle(tabStyle TabStyle) {
  186. s.tabStyle = tabStyle
  187. }
  188. // ModeApplier is the interface that wraps a representation of the terminal
  189. // mode. ApplyMode sets the terminal to this mode.
  190. type ModeApplier interface {
  191. ApplyMode() error
  192. }
  193. // SetCtrlCAborts sets whether Prompt on a supported terminal will return an
  194. // ErrPromptAborted when Ctrl-C is pressed. The default is false (will not
  195. // return when Ctrl-C is pressed). Unsupported terminals typically raise SIGINT
  196. // (and Prompt does not return) regardless of the value passed to SetCtrlCAborts.
  197. func (s *State) SetCtrlCAborts(aborts bool) {
  198. s.ctrlCAborts = aborts
  199. }
  200. // SetMultiLineMode sets whether line is auto-wrapped. The default is false (single line).
  201. func (s *State) SetMultiLineMode(mlmode bool) {
  202. s.multiLineMode = mlmode
  203. }
  204. // ShouldRestart is passed the error generated by readNext and returns true if
  205. // the the read should be restarted or false if the error should be returned.
  206. type ShouldRestart func(err error) bool
  207. // SetShouldRestart sets the restart function that Liner will call to determine
  208. // whether to retry the call to, or return the error returned by, readNext.
  209. func (s *State) SetShouldRestart(f ShouldRestart) {
  210. s.shouldRestart = f
  211. }
  212. func (s *State) promptUnsupported(p string) (string, error) {
  213. if !s.inputRedirected || !s.terminalSupported {
  214. fmt.Print(p)
  215. }
  216. linebuf, _, err := s.r.ReadLine()
  217. if err != nil {
  218. return "", err
  219. }
  220. return string(linebuf), nil
  221. }