2017-07-14 01:51:19 +00:00
|
|
|
package prompt
|
|
|
|
|
2018-02-18 13:27:59 +00:00
|
|
|
import (
|
|
|
|
"runtime"
|
2018-06-21 16:11:58 +00:00
|
|
|
|
2018-12-09 17:33:29 +00:00
|
|
|
"github.com/c-bata/go-prompt/internal/debug"
|
2018-12-09 11:42:33 +00:00
|
|
|
runewidth "github.com/mattn/go-runewidth"
|
2018-02-18 13:27:59 +00:00
|
|
|
)
|
2018-02-14 16:06:06 +00:00
|
|
|
|
2017-08-21 15:47:15 +00:00
|
|
|
// Render to render prompt information from state of Buffer.
|
2017-07-14 01:51:19 +00:00
|
|
|
type Render struct {
|
2018-02-12 11:00:48 +00:00
|
|
|
out ConsoleWriter
|
|
|
|
prefix string
|
|
|
|
livePrefixCallback func() (prefix string, useLivePrefix bool)
|
2019-08-26 13:41:23 +00:00
|
|
|
breakLineCallback func(*Document)
|
2018-02-12 11:00:48 +00:00
|
|
|
title string
|
|
|
|
row uint16
|
|
|
|
col uint16
|
2018-02-13 14:09:27 +00:00
|
|
|
|
|
|
|
previousCursor int
|
|
|
|
|
2018-02-12 11:00:48 +00:00
|
|
|
// colors,
|
2017-07-18 15:36:16 +00:00
|
|
|
prefixTextColor Color
|
|
|
|
prefixBGColor Color
|
|
|
|
inputTextColor Color
|
|
|
|
inputBGColor Color
|
|
|
|
previewSuggestionTextColor Color
|
|
|
|
previewSuggestionBGColor Color
|
|
|
|
suggestionTextColor Color
|
|
|
|
suggestionBGColor Color
|
|
|
|
selectedSuggestionTextColor Color
|
|
|
|
selectedSuggestionBGColor Color
|
|
|
|
descriptionTextColor Color
|
|
|
|
descriptionBGColor Color
|
|
|
|
selectedDescriptionTextColor Color
|
|
|
|
selectedDescriptionBGColor Color
|
2018-02-12 08:40:47 +00:00
|
|
|
scrollbarThumbColor Color
|
|
|
|
scrollbarBGColor Color
|
2017-07-15 11:22:56 +00:00
|
|
|
}
|
|
|
|
|
2017-08-21 15:47:15 +00:00
|
|
|
// Setup to initialize console output.
|
2017-07-15 11:22:56 +00:00
|
|
|
func (r *Render) Setup() {
|
2017-07-16 17:18:11 +00:00
|
|
|
if r.title != "" {
|
|
|
|
r.out.SetTitle(r.title)
|
2018-12-14 13:28:41 +00:00
|
|
|
debug.AssertNoError(r.out.Flush())
|
2017-07-15 11:22:56 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-02-12 11:00:48 +00:00
|
|
|
// getCurrentPrefix to get current prefix.
|
|
|
|
// If live-prefix is enabled, return live-prefix.
|
|
|
|
func (r *Render) getCurrentPrefix() string {
|
|
|
|
if prefix, ok := r.livePrefixCallback(); ok {
|
|
|
|
return prefix
|
|
|
|
}
|
|
|
|
return r.prefix
|
|
|
|
}
|
|
|
|
|
2017-07-15 16:18:40 +00:00
|
|
|
func (r *Render) renderPrefix() {
|
2017-07-18 16:16:51 +00:00
|
|
|
r.out.SetColor(r.prefixTextColor, r.prefixBGColor, false)
|
2018-02-12 11:00:48 +00:00
|
|
|
r.out.WriteStr(r.getCurrentPrefix())
|
2017-07-18 16:16:51 +00:00
|
|
|
r.out.SetColor(DefaultColor, DefaultColor, false)
|
2017-07-15 16:18:40 +00:00
|
|
|
}
|
|
|
|
|
2017-08-21 15:47:15 +00:00
|
|
|
// TearDown to clear title and erasing.
|
2017-07-15 11:22:56 +00:00
|
|
|
func (r *Render) TearDown() {
|
|
|
|
r.out.ClearTitle()
|
|
|
|
r.out.EraseDown()
|
2018-12-14 13:28:41 +00:00
|
|
|
debug.AssertNoError(r.out.Flush())
|
2017-07-15 08:37:54 +00:00
|
|
|
}
|
|
|
|
|
2017-07-15 12:43:04 +00:00
|
|
|
func (r *Render) prepareArea(lines int) {
|
2017-07-15 08:37:54 +00:00
|
|
|
for i := 0; i < lines; i++ {
|
|
|
|
r.out.ScrollDown()
|
|
|
|
}
|
|
|
|
for i := 0; i < lines; i++ {
|
|
|
|
r.out.ScrollUp()
|
|
|
|
}
|
|
|
|
return
|
2017-07-14 01:51:19 +00:00
|
|
|
}
|
|
|
|
|
2017-08-21 15:47:15 +00:00
|
|
|
// UpdateWinSize called when window size is changed.
|
2017-07-15 09:03:18 +00:00
|
|
|
func (r *Render) UpdateWinSize(ws *WinSize) {
|
2017-07-14 01:51:19 +00:00
|
|
|
r.row = ws.Row
|
|
|
|
r.col = ws.Col
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-07-17 16:14:03 +00:00
|
|
|
func (r *Render) renderWindowTooSmall() {
|
|
|
|
r.out.CursorGoTo(0, 0)
|
|
|
|
r.out.EraseScreen()
|
2017-07-18 16:16:51 +00:00
|
|
|
r.out.SetColor(DarkRed, White, false)
|
2017-07-17 16:14:03 +00:00
|
|
|
r.out.WriteStr("Your console window is too small...")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-08-09 12:33:47 +00:00
|
|
|
func (r *Render) renderCompletion(buf *Buffer, completions *CompletionManager) {
|
2018-02-13 14:09:27 +00:00
|
|
|
suggestions := completions.GetSuggestions()
|
|
|
|
if len(completions.GetSuggestions()) == 0 {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
prefix := r.getCurrentPrefix()
|
|
|
|
formatted, width := formatSuggestions(
|
|
|
|
suggestions,
|
2018-06-21 16:11:58 +00:00
|
|
|
int(r.col)-runewidth.StringWidth(prefix)-1, // -1 means a width of scrollbar
|
2018-02-13 14:09:27 +00:00
|
|
|
)
|
|
|
|
// +1 means a width of scrollbar.
|
2018-02-13 16:14:22 +00:00
|
|
|
width++
|
2018-02-13 14:09:27 +00:00
|
|
|
|
2018-02-13 14:34:00 +00:00
|
|
|
windowHeight := len(formatted)
|
2018-02-12 08:40:47 +00:00
|
|
|
if windowHeight > int(completions.max) {
|
|
|
|
windowHeight = int(completions.max)
|
|
|
|
}
|
2018-02-13 14:09:27 +00:00
|
|
|
formatted = formatted[completions.verticalScroll : completions.verticalScroll+windowHeight]
|
|
|
|
r.prepareArea(windowHeight)
|
|
|
|
|
2018-06-21 16:11:58 +00:00
|
|
|
cursor := runewidth.StringWidth(prefix) + runewidth.StringWidth(buf.Document().TextBeforeCursor())
|
2018-02-13 14:09:27 +00:00
|
|
|
x, _ := r.toPos(cursor)
|
|
|
|
if x+width >= int(r.col) {
|
2018-02-14 16:06:06 +00:00
|
|
|
cursor = r.backward(cursor, x+width-int(r.col))
|
2018-02-13 14:09:27 +00:00
|
|
|
}
|
|
|
|
|
2018-02-12 08:40:47 +00:00
|
|
|
contentHeight := len(completions.tmp)
|
|
|
|
|
|
|
|
fractionVisible := float64(windowHeight) / float64(contentHeight)
|
|
|
|
fractionAbove := float64(completions.verticalScroll) / float64(contentHeight)
|
|
|
|
|
2018-02-13 14:09:27 +00:00
|
|
|
scrollbarHeight := int(clamp(float64(windowHeight), 1, float64(windowHeight)*fractionVisible))
|
2018-02-12 08:40:47 +00:00
|
|
|
scrollbarTop := int(float64(windowHeight) * fractionAbove)
|
|
|
|
|
|
|
|
isScrollThumb := func(row int) bool {
|
|
|
|
return scrollbarTop <= row && row <= scrollbarTop+scrollbarHeight
|
2017-07-17 12:54:39 +00:00
|
|
|
}
|
|
|
|
|
2018-02-12 08:40:47 +00:00
|
|
|
selected := completions.selected - completions.verticalScroll
|
2017-07-18 16:16:51 +00:00
|
|
|
r.out.SetColor(White, Cyan, false)
|
2018-02-13 14:09:27 +00:00
|
|
|
for i := 0; i < windowHeight; i++ {
|
2017-07-15 09:51:33 +00:00
|
|
|
r.out.CursorDown(1)
|
2018-02-12 08:40:47 +00:00
|
|
|
if i == selected {
|
2017-07-18 16:16:51 +00:00
|
|
|
r.out.SetColor(r.selectedSuggestionTextColor, r.selectedSuggestionBGColor, true)
|
2017-07-15 13:44:10 +00:00
|
|
|
} else {
|
2017-07-18 16:16:51 +00:00
|
|
|
r.out.SetColor(r.suggestionTextColor, r.suggestionBGColor, false)
|
2017-07-15 13:44:10 +00:00
|
|
|
}
|
2017-07-18 15:36:16 +00:00
|
|
|
r.out.WriteStr(formatted[i].Text)
|
|
|
|
|
2018-02-12 08:40:47 +00:00
|
|
|
if i == selected {
|
2017-07-18 16:16:51 +00:00
|
|
|
r.out.SetColor(r.selectedDescriptionTextColor, r.selectedDescriptionBGColor, false)
|
2017-07-18 15:36:16 +00:00
|
|
|
} else {
|
2017-07-18 16:16:51 +00:00
|
|
|
r.out.SetColor(r.descriptionTextColor, r.descriptionBGColor, false)
|
2017-07-18 15:36:16 +00:00
|
|
|
}
|
|
|
|
r.out.WriteStr(formatted[i].Description)
|
2018-02-12 08:40:47 +00:00
|
|
|
|
|
|
|
if isScrollThumb(i) {
|
|
|
|
r.out.SetColor(DefaultColor, r.scrollbarThumbColor, false)
|
|
|
|
} else {
|
|
|
|
r.out.SetColor(DefaultColor, r.scrollbarBGColor, false)
|
|
|
|
}
|
|
|
|
r.out.WriteStr(" ")
|
2018-02-14 16:06:06 +00:00
|
|
|
r.out.SetColor(DefaultColor, DefaultColor, false)
|
|
|
|
|
|
|
|
r.lineWrap(cursor + width)
|
|
|
|
r.backward(cursor+width, width)
|
2017-07-15 09:51:33 +00:00
|
|
|
}
|
2018-02-13 14:09:27 +00:00
|
|
|
|
|
|
|
if x+width >= int(r.col) {
|
|
|
|
r.out.CursorForward(x + width - int(r.col))
|
2017-07-15 16:04:18 +00:00
|
|
|
}
|
2017-07-15 08:37:54 +00:00
|
|
|
|
2018-02-13 14:09:27 +00:00
|
|
|
r.out.CursorUp(windowHeight)
|
2017-07-18 16:16:51 +00:00
|
|
|
r.out.SetColor(DefaultColor, DefaultColor, false)
|
2017-07-15 09:51:33 +00:00
|
|
|
return
|
|
|
|
}
|
2017-07-15 08:37:54 +00:00
|
|
|
|
2017-08-21 15:47:15 +00:00
|
|
|
// Render renders to the console.
|
2017-08-09 12:33:47 +00:00
|
|
|
func (r *Render) Render(buffer *Buffer, completion *CompletionManager) {
|
2018-02-14 10:58:20 +00:00
|
|
|
// In situations where a pseudo tty is allocated (e.g. within a docker container),
|
2018-02-14 10:50:00 +00:00
|
|
|
// window size via TIOCGWINSZ is not immediately available and will result in 0,0 dimensions.
|
|
|
|
if r.col == 0 {
|
|
|
|
return
|
|
|
|
}
|
2018-12-14 13:28:41 +00:00
|
|
|
defer func() { debug.AssertNoError(r.out.Flush()) }()
|
2018-02-19 10:44:24 +00:00
|
|
|
r.move(r.previousCursor, 0)
|
2018-02-14 10:50:00 +00:00
|
|
|
|
2018-02-12 14:23:00 +00:00
|
|
|
line := buffer.Text()
|
2018-02-12 11:00:48 +00:00
|
|
|
prefix := r.getCurrentPrefix()
|
2018-06-21 16:11:58 +00:00
|
|
|
cursor := runewidth.StringWidth(prefix) + runewidth.StringWidth(line)
|
2018-02-12 11:00:48 +00:00
|
|
|
|
2018-02-14 10:50:00 +00:00
|
|
|
// prepare area
|
|
|
|
_, y := r.toPos(cursor)
|
2018-02-13 14:09:27 +00:00
|
|
|
|
2018-02-14 10:50:00 +00:00
|
|
|
h := y + 1 + int(completion.max)
|
|
|
|
if h > int(r.row) || completionMargin > int(r.col) {
|
|
|
|
r.renderWindowTooSmall()
|
|
|
|
return
|
2017-07-17 16:14:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Rendering
|
2018-02-18 13:27:59 +00:00
|
|
|
r.out.HideCursor()
|
|
|
|
defer r.out.ShowCursor()
|
|
|
|
|
2017-07-17 16:14:03 +00:00
|
|
|
r.renderPrefix()
|
2017-07-18 16:16:51 +00:00
|
|
|
r.out.SetColor(r.inputTextColor, r.inputBGColor, false)
|
2017-07-15 11:22:56 +00:00
|
|
|
r.out.WriteStr(line)
|
2017-07-18 16:16:51 +00:00
|
|
|
r.out.SetColor(DefaultColor, DefaultColor, false)
|
2018-02-14 16:06:06 +00:00
|
|
|
r.lineWrap(cursor)
|
2018-02-13 14:09:27 +00:00
|
|
|
|
2018-02-18 13:27:59 +00:00
|
|
|
r.out.EraseDown()
|
|
|
|
|
2018-06-21 16:11:58 +00:00
|
|
|
cursor = r.backward(cursor, runewidth.StringWidth(line)-buffer.DisplayCursorPosition())
|
2018-02-13 14:09:27 +00:00
|
|
|
|
2017-08-09 12:33:47 +00:00
|
|
|
r.renderCompletion(buffer, completion)
|
|
|
|
if suggest, ok := completion.GetSelectedSuggestion(); ok {
|
2018-06-23 19:29:58 +00:00
|
|
|
cursor = r.backward(cursor, runewidth.StringWidth(buffer.Document().GetWordBeforeCursorUntilSeparator(completion.wordSeparator)))
|
2018-02-13 14:09:27 +00:00
|
|
|
|
2017-07-18 16:16:51 +00:00
|
|
|
r.out.SetColor(r.previewSuggestionTextColor, r.previewSuggestionBGColor, false)
|
2017-08-09 12:33:47 +00:00
|
|
|
r.out.WriteStr(suggest.Text)
|
2017-07-18 16:16:51 +00:00
|
|
|
r.out.SetColor(DefaultColor, DefaultColor, false)
|
2018-06-21 16:11:58 +00:00
|
|
|
cursor += runewidth.StringWidth(suggest.Text)
|
2018-02-19 07:35:33 +00:00
|
|
|
|
|
|
|
rest := buffer.Document().TextAfterCursor()
|
|
|
|
r.out.WriteStr(rest)
|
2018-06-21 16:11:58 +00:00
|
|
|
cursor += runewidth.StringWidth(rest)
|
2018-02-14 16:06:06 +00:00
|
|
|
r.lineWrap(cursor)
|
2018-02-19 07:35:33 +00:00
|
|
|
|
2018-06-21 16:11:58 +00:00
|
|
|
cursor = r.backward(cursor, runewidth.StringWidth(rest))
|
2017-07-15 14:27:49 +00:00
|
|
|
}
|
2018-02-13 14:09:27 +00:00
|
|
|
r.previousCursor = cursor
|
2017-07-15 11:22:56 +00:00
|
|
|
}
|
|
|
|
|
2017-08-21 15:47:15 +00:00
|
|
|
// BreakLine to break line.
|
2017-07-18 11:48:50 +00:00
|
|
|
func (r *Render) BreakLine(buffer *Buffer) {
|
|
|
|
// Erasing and Render
|
2018-06-21 16:11:58 +00:00
|
|
|
cursor := runewidth.StringWidth(buffer.Document().TextBeforeCursor()) + runewidth.StringWidth(r.getCurrentPrefix())
|
2018-02-13 14:09:27 +00:00
|
|
|
r.clear(cursor)
|
2017-07-17 16:14:03 +00:00
|
|
|
r.renderPrefix()
|
2017-07-18 16:16:51 +00:00
|
|
|
r.out.SetColor(r.inputTextColor, r.inputBGColor, false)
|
2017-07-16 19:55:05 +00:00
|
|
|
r.out.WriteStr(buffer.Document().Text + "\n")
|
2017-07-18 16:16:51 +00:00
|
|
|
r.out.SetColor(DefaultColor, DefaultColor, false)
|
2018-12-14 13:28:41 +00:00
|
|
|
debug.AssertNoError(r.out.Flush())
|
2019-08-26 13:41:23 +00:00
|
|
|
if r.breakLineCallback != nil {
|
|
|
|
r.breakLineCallback(buffer.Document())
|
2019-08-16 00:09:32 +00:00
|
|
|
}
|
2018-02-13 14:09:27 +00:00
|
|
|
|
|
|
|
r.previousCursor = 0
|
|
|
|
}
|
|
|
|
|
2018-02-19 10:44:24 +00:00
|
|
|
// clear erases the screen from a beginning of input
|
|
|
|
// even if there is line break which means input length exceeds a window's width.
|
2018-02-13 14:09:27 +00:00
|
|
|
func (r *Render) clear(cursor int) {
|
2018-02-19 10:44:24 +00:00
|
|
|
r.move(cursor, 0)
|
2018-02-13 14:09:27 +00:00
|
|
|
r.out.EraseDown()
|
|
|
|
}
|
|
|
|
|
2018-02-19 10:44:24 +00:00
|
|
|
// backward moves cursor to backward from a current cursor position
|
|
|
|
// regardless there is a line break.
|
2018-02-13 14:09:27 +00:00
|
|
|
func (r *Render) backward(from, n int) int {
|
|
|
|
return r.move(from, from-n)
|
|
|
|
}
|
|
|
|
|
2018-02-19 10:44:24 +00:00
|
|
|
// move moves cursor to specified position from the beginning of input
|
|
|
|
// even if there is a line break.
|
2018-02-13 14:09:27 +00:00
|
|
|
func (r *Render) move(from, to int) int {
|
2018-02-14 16:06:06 +00:00
|
|
|
fromX, fromY := r.toPos(from)
|
2018-02-13 14:09:27 +00:00
|
|
|
toX, toY := r.toPos(to)
|
|
|
|
|
|
|
|
r.out.CursorUp(fromY - toY)
|
2018-02-14 16:06:06 +00:00
|
|
|
r.out.CursorBackward(fromX - toX)
|
2018-02-13 14:09:27 +00:00
|
|
|
return to
|
|
|
|
}
|
|
|
|
|
|
|
|
// toPos returns the relative position from the beginning of the string.
|
|
|
|
func (r *Render) toPos(cursor int) (x, y int) {
|
|
|
|
col := int(r.col)
|
2018-02-14 16:06:06 +00:00
|
|
|
return cursor % col, cursor / col
|
|
|
|
}
|
2018-02-13 14:09:27 +00:00
|
|
|
|
2018-02-14 16:06:06 +00:00
|
|
|
func (r *Render) lineWrap(cursor int) {
|
|
|
|
if runtime.GOOS != "windows" && cursor > 0 && cursor%int(r.col) == 0 {
|
|
|
|
r.out.WriteRaw([]byte{'\n'})
|
2018-02-13 14:09:27 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func clamp(high, low, x float64) float64 {
|
|
|
|
switch {
|
|
|
|
case high < x:
|
|
|
|
return high
|
|
|
|
case x < low:
|
|
|
|
return low
|
|
|
|
default:
|
|
|
|
return x
|
|
|
|
}
|
2017-07-18 11:48:50 +00:00
|
|
|
}
|