histterm.go 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  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. package termutil
  10. import (
  11. "bufio"
  12. "io/ioutil"
  13. "os"
  14. "strings"
  15. "devt.de/krotik/common/datautil"
  16. "devt.de/krotik/common/fileutil"
  17. "devt.de/krotik/common/stringutil"
  18. "devt.de/krotik/common/termutil/getch"
  19. )
  20. /*
  21. DefaultHistoryBufferSize is the default history buffer size in lines
  22. */
  23. var DefaultHistoryBufferSize = 100
  24. /*
  25. historyLineTerminalMixin adds history functionality to a given ConsoleLineTerminal
  26. */
  27. type historyLineTerminalMixin struct {
  28. ConsoleLineTerminal // Terminal which is being extended
  29. histFile string // File containing the history
  30. history *datautil.RingBuffer // Buffer containing the history
  31. historyPointer int // Pointer into history buffer
  32. lastEntry string // Temporary storage for last entry
  33. ignoreLine func(string) bool // Ignore line function
  34. }
  35. /*
  36. AddHistoryMixin adds history support for a given ConsoleLineTerminal. History
  37. is collected with every line and persisted in a file. The user can scroll
  38. through the history using the cursor keys up and down. The client can optionally
  39. define a ignoreLine function which causes a line to be ignored if it returns true.
  40. */
  41. func AddHistoryMixin(term ConsoleLineTerminal, histFile string,
  42. ignoreLine func(string) bool) (ConsoleLineTerminal, error) {
  43. var err error
  44. histterm := &historyLineTerminalMixin{term, histFile,
  45. datautil.NewRingBuffer(DefaultHistoryBufferSize), 0, "", ignoreLine}
  46. // Add key handler
  47. histterm.AddKeyHandler(histterm.handleKeyInput)
  48. if histFile != "" {
  49. if ok, err := fileutil.PathExists(histFile); err == nil && ok {
  50. var file *os.File
  51. // Read old history
  52. if file, err = os.Open(histFile); err == nil {
  53. defer file.Close()
  54. scanner := bufio.NewScanner(file)
  55. for scanner.Scan() {
  56. histterm.history.Add(scanner.Text())
  57. }
  58. histterm.historyPointer = histterm.history.Size()
  59. }
  60. }
  61. }
  62. return histterm, err
  63. }
  64. /*
  65. handleKeyInput handles the key input for the history mixin.
  66. */
  67. func (ht *historyLineTerminalMixin) handleKeyInput(e *getch.KeyEvent, buf []rune) (bool, []rune, error) {
  68. var ret []rune
  69. if e.Code == getch.KeyArrowUp && ht.historyPointer > 0 {
  70. // Go up in history
  71. if ht.historyPointer == ht.history.Size() {
  72. // Save the current entered text
  73. ht.lastEntry = stringutil.RuneSliceToString(buf)
  74. }
  75. ht.historyPointer--
  76. histLine := ht.history.Get(ht.historyPointer).(string)
  77. ret = stringutil.StringToRuneSlice(histLine)
  78. } else if e.Code == getch.KeyArrowDown && ht.historyPointer < ht.history.Size()-1 {
  79. // Go down in history
  80. ht.historyPointer++
  81. histLine := ht.history.Get(ht.historyPointer).(string)
  82. ret = stringutil.StringToRuneSlice(histLine)
  83. } else if e.Code == getch.KeyArrowDown && ht.historyPointer == ht.history.Size()-1 {
  84. // Restore the last entry from where we started
  85. ret = stringutil.StringToRuneSlice(ht.lastEntry)
  86. ht.historyPointer++
  87. }
  88. return ret != nil, ret, nil
  89. }
  90. /*
  91. NextLine lets the user produce the next line in the terminal. All entered
  92. characters are echoed. The line is finished if the user presses return or
  93. pastes in a newline character. The final newline is echoed. If single
  94. character input via getch is not available then the code falls back to a
  95. simple line input from stdin. If single character input is available then
  96. the entered lines are safed in a history buffer which can be accessed via the
  97. up and down arrow keys.
  98. */
  99. func (ht *historyLineTerminalMixin) NextLine() (string, error) {
  100. line, err := ht.ConsoleLineTerminal.NextLine()
  101. if strings.TrimSpace(line) != "" && (ht.ignoreLine == nil || !ht.ignoreLine(line)) {
  102. // Safe entered line
  103. ht.history.Add(line)
  104. ht.lastEntry = ""
  105. ht.historyPointer = ht.history.Size()
  106. }
  107. if ht.histFile != "" {
  108. ioutil.WriteFile(ht.histFile, []byte(ht.history.String()), 0600)
  109. }
  110. return line, err
  111. }