term.go 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. /*
  2. * Public Domain Software
  3. *
  4. * I (Matthias Ladkau) am the author of the source code in this file.
  5. * I have placed the source code in this file in the public domain.
  6. *
  7. * For further information see: http://creativecommons.org/publicdomain/zero/1.0/
  8. */
  9. /*
  10. Package termutil contains common function for terminal operations.
  11. */
  12. package termutil
  13. import (
  14. "bufio"
  15. "fmt"
  16. "io"
  17. "os"
  18. "unicode/utf8"
  19. "devt.de/krotik/common/stringutil"
  20. "devt.de/krotik/common/termutil/getch"
  21. )
  22. /*
  23. KeyHandler handles specific key events. KeyHandlers are used to extend the
  24. functionality of the normal ConsoleLineTerminal. Returns if the event was
  25. consumed (no further handling possible), a new input buffer and any errors
  26. that might have occurred. The new input buffer is ignored if it is nil.
  27. */
  28. type KeyHandler func(*getch.KeyEvent, []rune) (bool, []rune, error)
  29. /*
  30. ConsoleLineTerminal is the most common console terminal implementation. The
  31. user types input and a chosen backend records the input by key. It has a
  32. graceful fallback to a standard line reader for all other platforms. The
  33. functionality can be extended by adding key handlers.
  34. Example code:
  35. clt, err := termutil.NewConsoleLineTerminal(os.Stdout)
  36. if err == nil {
  37. // Add history functionality
  38. clt, err = termutil.AddHistoryMixin(clt, "", func(s string) bool {
  39. return s == "q"
  40. })
  41. if err == nil {
  42. rootDict := termutil.NewWordListDict([]string{"ll", "dir", "test",
  43. "test1", "test2"})
  44. chooser := func(lineWords []string,
  45. dictCache map[string]termutil.Dict) (termutil.Dict, error) {
  46. if len(lineWords) == 1 {
  47. return rootDict, nil
  48. }
  49. return termutil.NewWordListDict([]string{
  50. fmt.Sprintf("file4-%v", len(lineWords)), "file2",
  51. "file1", "directory"}), nil
  52. }
  53. dict := termutil.NewMultiWordDict(chooser, nil)
  54. clt, err = termutil.AddAutoCompleteMixin(clt, dict)
  55. if err == nil {
  56. if err = clt.StartTerm(); err == nil {
  57. var line string
  58. defer clt.StopTerm()
  59. line, err = clt.NextLine()
  60. for err == nil && line != "q" {
  61. fmt.Println("###", line)
  62. line, err = clt.NextLine()
  63. }
  64. }
  65. }
  66. }
  67. }
  68. if err != nil {
  69. fmt.Println(err)
  70. }
  71. */
  72. type ConsoleLineTerminal interface {
  73. /*
  74. StartTerm prepares a new terminal session. This call initialises the tty
  75. on Linux or retrieves an event object on Windows.
  76. */
  77. StartTerm() error
  78. /*
  79. AddKeyHandler adds a new KeyHandler to this ConsoleLineTerminal.
  80. */
  81. AddKeyHandler(handler KeyHandler)
  82. /*
  83. NextLine lets the user produce the next line in the terminal. All entered
  84. characters are echoed. The line is finished if the user presses return or
  85. pastes in a newline character. The final newline is echoed. If single
  86. character input via getch is not available then the code falls back to a
  87. simple line input from stdin.
  88. */
  89. NextLine() (string, error)
  90. /*
  91. NextLinePrompt lets the user produce the next line in the terminal with a
  92. special prompt. All entered characters are echoed if echo is 0x0 otherwise
  93. the echo character is written. The line is finished if the user presses
  94. return or pastes in a newline character. The final newline is echoed. If
  95. single character input via getch is not available then the code falls back
  96. to a simple line input from stdin.
  97. */
  98. NextLinePrompt(prompt string, echo rune) (string, error)
  99. /*
  100. WriteString write a string on this terminal.
  101. */
  102. WriteString(s string)
  103. /*
  104. Write writes len(p) bytes from p to the terminal.
  105. */
  106. Write(p []byte) (n int, err error)
  107. /*
  108. StopTerm finishes the current terminal session. This call returns the tty
  109. on Linux to its original state and closes all open handles on all platforms.
  110. */
  111. StopTerm()
  112. }
  113. /*
  114. consoleLineTerminal is the main ConsoleLineTerminal implementation.
  115. */
  116. type consoleLineTerminal struct {
  117. console io.Writer // Console to write to
  118. prompt string // Terminal prompt to display
  119. fallback bool // Flag if we can use getch or should do fallback
  120. handlers []KeyHandler // List of KeyHandlers which provide extra functionality
  121. }
  122. /*
  123. NewConsoleLineTerminal creates a new basic ConsoleLineTerminal.
  124. */
  125. func NewConsoleLineTerminal(console io.Writer) (ConsoleLineTerminal, error) {
  126. ret := &consoleLineTerminal{console, ">>>", false, []KeyHandler{}}
  127. return ret, nil
  128. }
  129. /*
  130. AddKeyHandler adds a new KeyHandler to this ConsoleLineTerminal.
  131. */
  132. func (clr *consoleLineTerminal) AddKeyHandler(handler KeyHandler) {
  133. clr.handlers = append(clr.handlers, handler)
  134. }
  135. /*
  136. StartTerm prepares a new terminal session. This call initialises the tty on
  137. Linux or retrieves an event object on Windows.
  138. */
  139. func (clr *consoleLineTerminal) StartTerm() error {
  140. // Initialise getch
  141. err := getchStart()
  142. if err != nil {
  143. // Activate fallback
  144. clr.fallback = true
  145. err = nil
  146. }
  147. return err
  148. }
  149. /*
  150. WriteString write a string on this terminal.
  151. */
  152. func (clr *consoleLineTerminal) WriteString(s string) {
  153. clr.Write([]byte(s))
  154. }
  155. /*
  156. Write writes len(p) bytes from p to the terminal.
  157. */
  158. func (clr *consoleLineTerminal) Write(p []byte) (n int, err error) {
  159. return fmt.Fprint(clr.console, string(p))
  160. }
  161. /*
  162. StopTerm finishes the current terminal session. This call returns the tty on
  163. Linux to its original state and closes all open handles on all platforms.
  164. */
  165. func (clr *consoleLineTerminal) StopTerm() {
  166. getchStop()
  167. }
  168. /*
  169. NextLine lets the user produce the next line in the terminal. All entered characters
  170. are echoed. The line is finished if the user presses return or pastes in a newline
  171. character. The final newline is echoed. If single character input via getch is not
  172. available then the code falls back to a simple line input from stdin.
  173. */
  174. func (clr *consoleLineTerminal) NextLine() (string, error) {
  175. return clr.NextLinePrompt(clr.prompt, 0x0)
  176. }
  177. /*
  178. NextLinePrompt lets the user produce the next line in the terminal with a
  179. special prompt. All entered characters are echoed if echo is 0x0 otherwise
  180. the echo character is written. The line is finished if the user presses
  181. return or pastes in a newline character. The final newline is echoed. If
  182. single character input via getch is not available then the code falls back
  183. to a simple line input from stdin.
  184. */
  185. func (clr *consoleLineTerminal) NextLinePrompt(prompt string, echo rune) (string, error) {
  186. var err error
  187. var e *getch.KeyEvent
  188. // Write out prompt
  189. fmt.Fprint(clr.console, prompt)
  190. if clr.fallback {
  191. if echo != 0x0 {
  192. // Input characters cannot be masked in fallback mode
  193. return "", fmt.Errorf("Cannot mask input characters")
  194. }
  195. // Use the fallback solution
  196. scanner := bufio.NewScanner(stdin)
  197. scanner.Scan()
  198. return scanner.Text(), nil
  199. }
  200. var buf []rune
  201. var lastWrite int
  202. cursorPos := 0
  203. addToBuf := func(t rune) {
  204. buf = append(buf[:cursorPos], append([]rune{t}, buf[cursorPos:]...)...)
  205. cursorPos++
  206. }
  207. delLeftFromCursor := func() {
  208. buf = append(buf[:cursorPos-1], buf[cursorPos:]...)
  209. cursorPos--
  210. }
  211. delRightFromCursor := func() {
  212. buf = append(buf[:cursorPos], buf[cursorPos+1:]...)
  213. }
  214. MainGetchLoop:
  215. // Main loop exits on error, Enter key or EOT (End of transmission) (CTRL+d)
  216. for (e == nil || (e.Code != getch.KeyEnter && e.Rune != 0x4)) && err == nil {
  217. e, err = getchGetch()
  218. if _, ok := err.(*getch.ErrUnknownEscapeSequence); ok {
  219. // Ignore unknown escape sequences
  220. err = nil
  221. continue
  222. }
  223. if err == nil {
  224. // Check KeyHandlers
  225. for _, h := range clr.handlers {
  226. var consumed bool
  227. var newBuf []rune
  228. consumed, newBuf, err = h(e, buf)
  229. if newBuf != nil {
  230. buf = newBuf
  231. cursorPos = len(newBuf)
  232. }
  233. if consumed {
  234. lastWrite = clr.output(prompt, buf, cursorPos, lastWrite)
  235. continue MainGetchLoop
  236. }
  237. }
  238. }
  239. if err == nil {
  240. if e.Rune != 0x0 {
  241. // Normal case a printable character was typed
  242. if len(e.RawBuf) == 0 {
  243. addToBuf(e.Rune)
  244. } else {
  245. // Handle copy & paste and quick typing
  246. for _, r := range string(e.RawBuf) {
  247. addToBuf(r)
  248. }
  249. }
  250. } else if e.Code == getch.KeyArrowLeft && cursorPos > 0 {
  251. cursorPos--
  252. } else if e.Code == getch.KeyArrowRight && cursorPos < len(buf) {
  253. cursorPos++
  254. } else if e.Code == getch.KeyEnd {
  255. cursorPos = len(buf)
  256. } else if e.Code == getch.KeyHome {
  257. cursorPos = 0
  258. } else if e.Code == getch.KeyDelete && cursorPos < len(buf) {
  259. // Delete next character
  260. delRightFromCursor()
  261. } else if e.Code == getch.KeyBackspace && cursorPos > 0 {
  262. // Delete last character
  263. delLeftFromCursor()
  264. } else if !e.Alt && !e.Shift && !e.Ctrl &&
  265. e.Rune == 0x0 && e.Code == "" {
  266. // Just append a space
  267. addToBuf(' ')
  268. }
  269. if e.Rune != 0x4 { // Do not echo EOT
  270. var outBuf []rune
  271. if echo != 0x0 {
  272. // Fill up the output buffer with the echo rune
  273. outBuf = make([]rune, len(buf))
  274. for i := range buf {
  275. outBuf[i] = echo
  276. }
  277. } else {
  278. outBuf = buf
  279. }
  280. lastWrite = clr.output(prompt, outBuf, cursorPos, lastWrite)
  281. }
  282. }
  283. }
  284. // Write final newline to be consistent with fallback line input
  285. fmt.Fprintln(clr.console, "")
  286. return stringutil.RuneSliceToString(buf), err
  287. }
  288. /*
  289. output writes the current line in the terminal.
  290. */
  291. func (clr *consoleLineTerminal) output(prompt string, buf []rune, cursorPos int, toClear int) int {
  292. promptLen := utf8.RuneCountInString(prompt)
  293. // Remove previous prompt text (on same line)
  294. fmt.Fprint(clr.console, "\r")
  295. fmt.Fprint(clr.console, stringutil.GenerateRollingString(" ", toClear))
  296. fmt.Fprint(clr.console, "\r")
  297. fmt.Fprint(clr.console, prompt)
  298. fmt.Fprintf(clr.console, stringutil.RuneSliceToString(buf))
  299. // Position the cursor
  300. if _, y, err := getch.CursorPosition(); err == nil {
  301. getch.SetCursorPosition(promptLen+cursorPos, y)
  302. }
  303. return promptLen + len(buf)
  304. }
  305. // Low-level input interfaces
  306. // ==========================
  307. var stdin io.Reader = os.Stdin
  308. var getchStart = getch.Start
  309. var getchStop = getch.Stop
  310. var getchGetch = getch.Getch