render.go 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. package prompt
  2. import (
  3. "runtime"
  4. "github.com/c-bata/go-prompt/internal/debug"
  5. runewidth "github.com/mattn/go-runewidth"
  6. )
  7. // Render to render prompt information from state of Buffer.
  8. type Render struct {
  9. out ConsoleWriter
  10. prefix string
  11. livePrefixCallback func() (prefix string, useLivePrefix bool)
  12. breakLineCallback func(*Document)
  13. title string
  14. row uint16
  15. col uint16
  16. previousCursor int
  17. // colors,
  18. prefixTextColor Color
  19. prefixBGColor Color
  20. inputTextColor Color
  21. inputBGColor Color
  22. previewSuggestionTextColor Color
  23. previewSuggestionBGColor Color
  24. suggestionTextColor Color
  25. suggestionBGColor Color
  26. selectedSuggestionTextColor Color
  27. selectedSuggestionBGColor Color
  28. descriptionTextColor Color
  29. descriptionBGColor Color
  30. selectedDescriptionTextColor Color
  31. selectedDescriptionBGColor Color
  32. scrollbarThumbColor Color
  33. scrollbarBGColor Color
  34. }
  35. // Setup to initialize console output.
  36. func (r *Render) Setup() {
  37. if r.title != "" {
  38. r.out.SetTitle(r.title)
  39. debug.AssertNoError(r.out.Flush())
  40. }
  41. }
  42. // getCurrentPrefix to get current prefix.
  43. // If live-prefix is enabled, return live-prefix.
  44. func (r *Render) getCurrentPrefix() string {
  45. if prefix, ok := r.livePrefixCallback(); ok {
  46. return prefix
  47. }
  48. return r.prefix
  49. }
  50. func (r *Render) renderPrefix() {
  51. r.out.SetColor(r.prefixTextColor, r.prefixBGColor, false)
  52. r.out.WriteStr(r.getCurrentPrefix())
  53. r.out.SetColor(DefaultColor, DefaultColor, false)
  54. }
  55. // TearDown to clear title and erasing.
  56. func (r *Render) TearDown() {
  57. r.out.ClearTitle()
  58. r.out.EraseDown()
  59. debug.AssertNoError(r.out.Flush())
  60. }
  61. func (r *Render) prepareArea(lines int) {
  62. for i := 0; i < lines; i++ {
  63. r.out.ScrollDown()
  64. }
  65. for i := 0; i < lines; i++ {
  66. r.out.ScrollUp()
  67. }
  68. }
  69. // UpdateWinSize called when window size is changed.
  70. func (r *Render) UpdateWinSize(ws *WinSize) {
  71. r.row = ws.Row
  72. r.col = ws.Col
  73. }
  74. func (r *Render) renderWindowTooSmall() {
  75. r.out.CursorGoTo(0, 0)
  76. r.out.EraseScreen()
  77. r.out.SetColor(DarkRed, White, false)
  78. r.out.WriteStr("Your console window is too small...")
  79. }
  80. func (r *Render) renderCompletion(buf *Buffer, completions *CompletionManager) {
  81. suggestions := completions.GetSuggestions()
  82. if len(completions.GetSuggestions()) == 0 {
  83. return
  84. }
  85. prefix := r.getCurrentPrefix()
  86. formatted, width := formatSuggestions(
  87. suggestions,
  88. int(r.col)-runewidth.StringWidth(prefix)-1, // -1 means a width of scrollbar
  89. )
  90. // +1 means a width of scrollbar.
  91. width++
  92. windowHeight := len(formatted)
  93. if windowHeight > int(completions.max) {
  94. windowHeight = int(completions.max)
  95. }
  96. formatted = formatted[completions.verticalScroll : completions.verticalScroll+windowHeight]
  97. r.prepareArea(windowHeight)
  98. cursor := runewidth.StringWidth(prefix) + runewidth.StringWidth(buf.Document().TextBeforeCursor())
  99. x, _ := r.toPos(cursor)
  100. if x+width >= int(r.col) {
  101. cursor = r.backward(cursor, x+width-int(r.col))
  102. }
  103. contentHeight := len(completions.tmp)
  104. fractionVisible := float64(windowHeight) / float64(contentHeight)
  105. fractionAbove := float64(completions.verticalScroll) / float64(contentHeight)
  106. scrollbarHeight := int(clamp(float64(windowHeight), 1, float64(windowHeight)*fractionVisible))
  107. scrollbarTop := int(float64(windowHeight) * fractionAbove)
  108. isScrollThumb := func(row int) bool {
  109. return scrollbarTop <= row && row <= scrollbarTop+scrollbarHeight
  110. }
  111. selected := completions.selected - completions.verticalScroll
  112. r.out.SetColor(White, Cyan, false)
  113. for i := 0; i < windowHeight; i++ {
  114. r.out.CursorDown(1)
  115. if i == selected {
  116. r.out.SetColor(r.selectedSuggestionTextColor, r.selectedSuggestionBGColor, true)
  117. } else {
  118. r.out.SetColor(r.suggestionTextColor, r.suggestionBGColor, false)
  119. }
  120. r.out.WriteStr(formatted[i].Text)
  121. if i == selected {
  122. r.out.SetColor(r.selectedDescriptionTextColor, r.selectedDescriptionBGColor, false)
  123. } else {
  124. r.out.SetColor(r.descriptionTextColor, r.descriptionBGColor, false)
  125. }
  126. r.out.WriteStr(formatted[i].Description)
  127. if isScrollThumb(i) {
  128. r.out.SetColor(DefaultColor, r.scrollbarThumbColor, false)
  129. } else {
  130. r.out.SetColor(DefaultColor, r.scrollbarBGColor, false)
  131. }
  132. r.out.WriteStr(" ")
  133. r.out.SetColor(DefaultColor, DefaultColor, false)
  134. r.lineWrap(cursor + width)
  135. r.backward(cursor+width, width)
  136. }
  137. if x+width >= int(r.col) {
  138. r.out.CursorForward(x + width - int(r.col))
  139. }
  140. r.out.CursorUp(windowHeight)
  141. r.out.SetColor(DefaultColor, DefaultColor, false)
  142. }
  143. // Render renders to the console.
  144. func (r *Render) Render(buffer *Buffer, completion *CompletionManager) {
  145. // In situations where a pseudo tty is allocated (e.g. within a docker container),
  146. // window size via TIOCGWINSZ is not immediately available and will result in 0,0 dimensions.
  147. if r.col == 0 {
  148. return
  149. }
  150. defer func() { debug.AssertNoError(r.out.Flush()) }()
  151. r.move(r.previousCursor, 0)
  152. line := buffer.Text()
  153. prefix := r.getCurrentPrefix()
  154. cursor := runewidth.StringWidth(prefix) + runewidth.StringWidth(line)
  155. // prepare area
  156. _, y := r.toPos(cursor)
  157. h := y + 1 + int(completion.max)
  158. if h > int(r.row) || completionMargin > int(r.col) {
  159. r.renderWindowTooSmall()
  160. return
  161. }
  162. // Rendering
  163. r.out.HideCursor()
  164. defer r.out.ShowCursor()
  165. r.renderPrefix()
  166. r.out.SetColor(r.inputTextColor, r.inputBGColor, false)
  167. r.out.WriteStr(line)
  168. r.out.SetColor(DefaultColor, DefaultColor, false)
  169. r.lineWrap(cursor)
  170. r.out.EraseDown()
  171. cursor = r.backward(cursor, runewidth.StringWidth(line)-buffer.DisplayCursorPosition())
  172. r.renderCompletion(buffer, completion)
  173. if suggest, ok := completion.GetSelectedSuggestion(); ok {
  174. cursor = r.backward(cursor, runewidth.StringWidth(buffer.Document().GetWordBeforeCursorUntilSeparator(completion.wordSeparator)))
  175. r.out.SetColor(r.previewSuggestionTextColor, r.previewSuggestionBGColor, false)
  176. r.out.WriteStr(suggest.Text)
  177. r.out.SetColor(DefaultColor, DefaultColor, false)
  178. cursor += runewidth.StringWidth(suggest.Text)
  179. rest := buffer.Document().TextAfterCursor()
  180. r.out.WriteStr(rest)
  181. cursor += runewidth.StringWidth(rest)
  182. r.lineWrap(cursor)
  183. cursor = r.backward(cursor, runewidth.StringWidth(rest))
  184. }
  185. r.previousCursor = cursor
  186. }
  187. // BreakLine to break line.
  188. func (r *Render) BreakLine(buffer *Buffer) {
  189. // Erasing and Render
  190. cursor := runewidth.StringWidth(buffer.Document().TextBeforeCursor()) + runewidth.StringWidth(r.getCurrentPrefix())
  191. r.clear(cursor)
  192. r.renderPrefix()
  193. r.out.SetColor(r.inputTextColor, r.inputBGColor, false)
  194. r.out.WriteStr(buffer.Document().Text + "\n")
  195. r.out.SetColor(DefaultColor, DefaultColor, false)
  196. debug.AssertNoError(r.out.Flush())
  197. if r.breakLineCallback != nil {
  198. r.breakLineCallback(buffer.Document())
  199. }
  200. r.previousCursor = 0
  201. }
  202. // clear erases the screen from a beginning of input
  203. // even if there is line break which means input length exceeds a window's width.
  204. func (r *Render) clear(cursor int) {
  205. r.move(cursor, 0)
  206. r.out.EraseDown()
  207. }
  208. // backward moves cursor to backward from a current cursor position
  209. // regardless there is a line break.
  210. func (r *Render) backward(from, n int) int {
  211. return r.move(from, from-n)
  212. }
  213. // move moves cursor to specified position from the beginning of input
  214. // even if there is a line break.
  215. func (r *Render) move(from, to int) int {
  216. fromX, fromY := r.toPos(from)
  217. toX, toY := r.toPos(to)
  218. r.out.CursorUp(fromY - toY)
  219. r.out.CursorBackward(fromX - toX)
  220. return to
  221. }
  222. // toPos returns the relative position from the beginning of the string.
  223. func (r *Render) toPos(cursor int) (x, y int) {
  224. col := int(r.col)
  225. return cursor % col, cursor / col
  226. }
  227. func (r *Render) lineWrap(cursor int) {
  228. if runtime.GOOS != "windows" && cursor > 0 && cursor%int(r.col) == 0 {
  229. r.out.WriteRaw([]byte{'\n'})
  230. }
  231. }
  232. func clamp(high, low, x float64) float64 {
  233. switch {
  234. case high < x:
  235. return high
  236. case x < low:
  237. return low
  238. default:
  239. return x
  240. }
  241. }