package prompt import ( "runtime" "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) 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) 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() 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() } return } // UpdateWinSize called when window size is changed. func (r *Render) UpdateWinSize(ws *WinSize) { r.row = ws.Row r.col = ws.Col return } 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...") return } 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) return } // 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 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) r.out.Flush() 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 } }