| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287 |
- package prompt
- import (
- "runtime"
- "github.com/c-bata/go-prompt/internal/debug"
- runewidth "github.com/mattn/go-runewidth"
- )
- // Render to render prompt information from state of Buffer.
- type Render struct {
- out ConsoleWriter
- prefix string
- livePrefixCallback func() (prefix string, useLivePrefix bool)
- breakLineCallback func(*Document)
- title string
- row uint16
- col uint16
- previousCursor int
- // colors,
- 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
- scrollbarThumbColor Color
- scrollbarBGColor Color
- }
- // Setup to initialize console output.
- func (r *Render) Setup() {
- if r.title != "" {
- r.out.SetTitle(r.title)
- debug.AssertNoError(r.out.Flush())
- }
- }
- // 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
- }
- func (r *Render) renderPrefix() {
- r.out.SetColor(r.prefixTextColor, r.prefixBGColor, false)
- r.out.WriteStr(r.getCurrentPrefix())
- r.out.SetColor(DefaultColor, DefaultColor, false)
- }
- // TearDown to clear title and erasing.
- func (r *Render) TearDown() {
- r.out.ClearTitle()
- r.out.EraseDown()
- debug.AssertNoError(r.out.Flush())
- }
- func (r *Render) prepareArea(lines int) {
- for i := 0; i < lines; i++ {
- r.out.ScrollDown()
- }
- for i := 0; i < lines; i++ {
- r.out.ScrollUp()
- }
- }
- // UpdateWinSize called when window size is changed.
- func (r *Render) UpdateWinSize(ws *WinSize) {
- r.row = ws.Row
- r.col = ws.Col
- }
- func (r *Render) renderWindowTooSmall() {
- r.out.CursorGoTo(0, 0)
- r.out.EraseScreen()
- r.out.SetColor(DarkRed, White, false)
- r.out.WriteStr("Your console window is too small...")
- }
- func (r *Render) renderCompletion(buf *Buffer, completions *CompletionManager) {
- suggestions := completions.GetSuggestions()
- if len(completions.GetSuggestions()) == 0 {
- return
- }
- prefix := r.getCurrentPrefix()
- formatted, width := formatSuggestions(
- suggestions,
- int(r.col)-runewidth.StringWidth(prefix)-1, // -1 means a width of scrollbar
- )
- // +1 means a width of scrollbar.
- width++
- windowHeight := len(formatted)
- if windowHeight > int(completions.max) {
- windowHeight = int(completions.max)
- }
- formatted = formatted[completions.verticalScroll : completions.verticalScroll+windowHeight]
- r.prepareArea(windowHeight)
- cursor := runewidth.StringWidth(prefix) + runewidth.StringWidth(buf.Document().TextBeforeCursor())
- x, _ := r.toPos(cursor)
- if x+width >= int(r.col) {
- cursor = r.backward(cursor, x+width-int(r.col))
- }
- contentHeight := len(completions.tmp)
- fractionVisible := float64(windowHeight) / float64(contentHeight)
- fractionAbove := float64(completions.verticalScroll) / float64(contentHeight)
- scrollbarHeight := int(clamp(float64(windowHeight), 1, float64(windowHeight)*fractionVisible))
- scrollbarTop := int(float64(windowHeight) * fractionAbove)
- isScrollThumb := func(row int) bool {
- return scrollbarTop <= row && row <= scrollbarTop+scrollbarHeight
- }
- selected := completions.selected - completions.verticalScroll
- r.out.SetColor(White, Cyan, false)
- for i := 0; i < windowHeight; i++ {
- r.out.CursorDown(1)
- if i == selected {
- r.out.SetColor(r.selectedSuggestionTextColor, r.selectedSuggestionBGColor, true)
- } else {
- r.out.SetColor(r.suggestionTextColor, r.suggestionBGColor, false)
- }
- r.out.WriteStr(formatted[i].Text)
- if i == selected {
- r.out.SetColor(r.selectedDescriptionTextColor, r.selectedDescriptionBGColor, false)
- } else {
- r.out.SetColor(r.descriptionTextColor, r.descriptionBGColor, false)
- }
- r.out.WriteStr(formatted[i].Description)
- if isScrollThumb(i) {
- r.out.SetColor(DefaultColor, r.scrollbarThumbColor, false)
- } else {
- r.out.SetColor(DefaultColor, r.scrollbarBGColor, false)
- }
- r.out.WriteStr(" ")
- r.out.SetColor(DefaultColor, DefaultColor, false)
- r.lineWrap(cursor + width)
- r.backward(cursor+width, width)
- }
- if x+width >= int(r.col) {
- r.out.CursorForward(x + width - int(r.col))
- }
- r.out.CursorUp(windowHeight)
- r.out.SetColor(DefaultColor, DefaultColor, false)
- }
- // Render renders to the console.
- func (r *Render) Render(buffer *Buffer, completion *CompletionManager) {
- // In situations where a pseudo tty is allocated (e.g. within a docker container),
- // window size via TIOCGWINSZ is not immediately available and will result in 0,0 dimensions.
- if r.col == 0 {
- return
- }
- defer func() { debug.AssertNoError(r.out.Flush()) }()
- r.move(r.previousCursor, 0)
- line := buffer.Text()
- prefix := r.getCurrentPrefix()
- cursor := runewidth.StringWidth(prefix) + runewidth.StringWidth(line)
- // prepare area
- _, y := r.toPos(cursor)
- h := y + 1 + int(completion.max)
- if h > int(r.row) || completionMargin > int(r.col) {
- r.renderWindowTooSmall()
- return
- }
- // Rendering
- r.out.HideCursor()
- defer r.out.ShowCursor()
- r.renderPrefix()
- r.out.SetColor(r.inputTextColor, r.inputBGColor, false)
- r.out.WriteStr(line)
- r.out.SetColor(DefaultColor, DefaultColor, false)
- r.lineWrap(cursor)
- r.out.EraseDown()
- cursor = r.backward(cursor, runewidth.StringWidth(line)-buffer.DisplayCursorPosition())
- r.renderCompletion(buffer, completion)
- if suggest, ok := completion.GetSelectedSuggestion(); ok {
- cursor = r.backward(cursor, runewidth.StringWidth(buffer.Document().GetWordBeforeCursorUntilSeparator(completion.wordSeparator)))
- r.out.SetColor(r.previewSuggestionTextColor, r.previewSuggestionBGColor, false)
- r.out.WriteStr(suggest.Text)
- r.out.SetColor(DefaultColor, DefaultColor, false)
- cursor += runewidth.StringWidth(suggest.Text)
- rest := buffer.Document().TextAfterCursor()
- r.out.WriteStr(rest)
- cursor += runewidth.StringWidth(rest)
- r.lineWrap(cursor)
- cursor = r.backward(cursor, runewidth.StringWidth(rest))
- }
- r.previousCursor = cursor
- }
- // BreakLine to break line.
- func (r *Render) BreakLine(buffer *Buffer) {
- // Erasing and Render
- cursor := runewidth.StringWidth(buffer.Document().TextBeforeCursor()) + runewidth.StringWidth(r.getCurrentPrefix())
- r.clear(cursor)
- r.renderPrefix()
- r.out.SetColor(r.inputTextColor, r.inputBGColor, false)
- r.out.WriteStr(buffer.Document().Text + "\n")
- r.out.SetColor(DefaultColor, DefaultColor, false)
- debug.AssertNoError(r.out.Flush())
- if r.breakLineCallback != nil {
- r.breakLineCallback(buffer.Document())
- }
- r.previousCursor = 0
- }
- // 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) {
- r.move(cursor, 0)
- r.out.EraseDown()
- }
- // 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)
- }
- // 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 {
- fromX, fromY := r.toPos(from)
- toX, toY := r.toPos(to)
- r.out.CursorUp(fromY - toY)
- 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)
- return cursor % col, cursor / col
- }
- 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
- }
- }
|