go-prompt/render.go

287 行
7.6 KiB
Go

2017-07-14 01:51:19 +00:00
package prompt
import (
"runtime"
2018-06-21 16:11:58 +00:00
"github.com/mattn/go-runewidth"
)
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 {
out ConsoleWriter
prefix string
livePrefixCallback func() (prefix string, useLivePrefix bool)
title string
row uint16
col uint16
previousCursor int
// 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)
2017-07-17 16:14:03 +00:00
r.out.Flush()
2017-07-15 11:22:56 +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)
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()
r.out.Flush()
2017-07-15 08:37:54 +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) {
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
)
// +1 means a width of scrollbar.
2018-02-13 16:14:22 +00:00
width++
windowHeight := len(formatted)
2018-02-12 08:40:47 +00:00
if windowHeight > int(completions.max) {
windowHeight = int(completions.max)
}
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())
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-12 08:40:47 +00:00
contentHeight := len(completions.tmp)
fractionVisible := float64(windowHeight) / float64(contentHeight)
fractionAbove := float64(completions.verticalScroll) / float64(contentHeight)
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)
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
}
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
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-02-19 10:44:24 +00:00
defer r.out.Flush()
r.move(r.previousCursor, 0)
2018-02-14 10:50:00 +00:00
line := buffer.Text()
prefix := r.getCurrentPrefix()
2018-06-21 16:11:58 +00:00
cursor := runewidth.StringWidth(prefix) + runewidth.StringWidth(line)
2018-02-14 10:50:00 +00:00
// prepare area
_, y := r.toPos(cursor)
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
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)
r.out.EraseDown()
2018-06-21 16:11:58 +00:00
cursor = r.backward(cursor, runewidth.StringWidth(line)-buffer.DisplayCursorPosition())
2017-08-09 12:33:47 +00:00
r.renderCompletion(buffer, completion)
if suggest, ok := completion.GetSelectedSuggestion(); ok {
cursor = r.backward(cursor, runewidth.StringWidth(buffer.Document().GetWordBeforeCursorUntilSeparator(completion.wordSeparator)))
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
}
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())
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)
2017-07-18 13:06:33 +00:00
r.out.Flush()
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.
func (r *Render) clear(cursor int) {
2018-02-19 10:44:24 +00:00
r.move(cursor, 0)
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.
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.
func (r *Render) move(from, to int) int {
2018-02-14 16:06:06 +00:00
fromX, fromY := r.toPos(from)
toX, toY := r.toPos(to)
r.out.CursorUp(fromY - toY)
2018-02-14 16:06:06 +00:00
r.out.CursorBackward(fromX - toX)
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-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'})
}
}
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
}