You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
195 lines
4.9 KiB
195 lines
4.9 KiB
package survey
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"git.yaojiankang.top/hupeh/gcz/survey/core"
|
|
"git.yaojiankang.top/hupeh/gcz/survey/terminal"
|
|
"golang.org/x/term"
|
|
)
|
|
|
|
type Renderer struct {
|
|
stdio terminal.Stdio
|
|
renderedErrors bytes.Buffer
|
|
renderedText bytes.Buffer
|
|
}
|
|
|
|
type ErrorTemplateData struct {
|
|
Error error
|
|
Icon Icon
|
|
}
|
|
|
|
var ErrorTemplate = `{{color .Icon.Format }}{{ .Icon.Text }} Sorry, your reply was invalid: {{ .Error.Error }}{{color "reset"}}
|
|
`
|
|
|
|
func (r *Renderer) WithStdio(stdio terminal.Stdio) {
|
|
r.stdio = stdio
|
|
}
|
|
|
|
func (r *Renderer) Stdio() terminal.Stdio {
|
|
return r.stdio
|
|
}
|
|
|
|
func (r *Renderer) NewRuneReader() *terminal.RuneReader {
|
|
return terminal.NewRuneReader(r.stdio)
|
|
}
|
|
|
|
func (r *Renderer) NewCursor() *terminal.Cursor {
|
|
return &terminal.Cursor{
|
|
In: r.stdio.In,
|
|
Out: r.stdio.Out,
|
|
}
|
|
}
|
|
|
|
func (r *Renderer) Error(config *PromptConfig, invalid error) error {
|
|
// cleanup the currently rendered errors
|
|
r.resetPrompt(r.countLines(r.renderedErrors))
|
|
r.renderedErrors.Reset()
|
|
|
|
// cleanup the rest of the prompt
|
|
r.resetPrompt(r.countLines(r.renderedText))
|
|
r.renderedText.Reset()
|
|
|
|
userOut, layoutOut, err := core.RunTemplate(ErrorTemplate, &ErrorTemplateData{
|
|
Error: invalid,
|
|
Icon: config.Icons.Error,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// send the message to the user
|
|
if _, err := fmt.Fprint(terminal.NewAnsiStdout(r.stdio.Out), userOut); err != nil {
|
|
return err
|
|
}
|
|
|
|
// add the printed text to the rendered error buffer so we can cleanup later
|
|
r.appendRenderedError(layoutOut)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *Renderer) OffsetCursor(offset int) {
|
|
cursor := r.NewCursor()
|
|
for offset > 0 {
|
|
cursor.PreviousLine(1)
|
|
offset--
|
|
}
|
|
}
|
|
|
|
func (r *Renderer) Render(tmpl string, data interface{}) error {
|
|
// cleanup the currently rendered text
|
|
lineCount := r.countLines(r.renderedText)
|
|
r.resetPrompt(lineCount)
|
|
r.renderedText.Reset()
|
|
|
|
// render the template summarizing the current state
|
|
userOut, layoutOut, err := core.RunTemplate(tmpl, data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// print the summary
|
|
if _, err := fmt.Fprint(terminal.NewAnsiStdout(r.stdio.Out), userOut); err != nil {
|
|
return err
|
|
}
|
|
|
|
// add the printed text to the rendered text buffer so we can cleanup later
|
|
r.AppendRenderedText(layoutOut)
|
|
|
|
// nothing went wrong
|
|
return nil
|
|
}
|
|
|
|
func (r *Renderer) RenderWithCursorOffset(tmpl string, data IterableOpts, opts []core.OptionAnswer, idx int) error {
|
|
cursor := r.NewCursor()
|
|
cursor.Restore() // clear any accessibility offsetting
|
|
|
|
if err := r.Render(tmpl, data); err != nil {
|
|
return err
|
|
}
|
|
cursor.Save()
|
|
|
|
offset := computeCursorOffset(MultiSelectQuestionTemplate, data, opts, idx, r.termWidthSafe())
|
|
r.OffsetCursor(offset)
|
|
|
|
return nil
|
|
}
|
|
|
|
// appendRenderedError appends text to the renderer's error buffer
|
|
// which is used to track what has been printed. It is not exported
|
|
// as errors should only be displayed via Error(config, error).
|
|
func (r *Renderer) appendRenderedError(text string) {
|
|
r.renderedErrors.WriteString(text)
|
|
}
|
|
|
|
// AppendRenderedText appends text to the renderer's text buffer
|
|
// which is used to track of what has been printed. The buffer is used
|
|
// to calculate how many lines to erase before updating the prompt.
|
|
func (r *Renderer) AppendRenderedText(text string) {
|
|
r.renderedText.WriteString(text)
|
|
}
|
|
|
|
func (r *Renderer) resetPrompt(lines int) {
|
|
// clean out current line in case tmpl didnt end in newline
|
|
cursor := r.NewCursor()
|
|
cursor.HorizontalAbsolute(0)
|
|
terminal.EraseLine(r.stdio.Out, terminal.ERASE_LINE_ALL)
|
|
// clean up what we left behind last time
|
|
for i := 0; i < lines; i++ {
|
|
cursor.PreviousLine(1)
|
|
terminal.EraseLine(r.stdio.Out, terminal.ERASE_LINE_ALL)
|
|
}
|
|
}
|
|
|
|
func (r *Renderer) termWidth() (int, error) {
|
|
fd := int(r.stdio.Out.Fd())
|
|
termWidth, _, err := term.GetSize(fd)
|
|
return termWidth, err
|
|
}
|
|
|
|
func (r *Renderer) termWidthSafe() int {
|
|
w, err := r.termWidth()
|
|
if err != nil || w == 0 {
|
|
// if we got an error due to terminal.GetSize not being supported
|
|
// on current platform then just assume a very wide terminal
|
|
w = 10000
|
|
}
|
|
return w
|
|
}
|
|
|
|
// countLines will return the count of `\n` with the addition of any
|
|
// lines that have wrapped due to narrow terminal width
|
|
func (r *Renderer) countLines(buf bytes.Buffer) int {
|
|
w := r.termWidthSafe()
|
|
|
|
bufBytes := buf.Bytes()
|
|
|
|
count := 0
|
|
curr := 0
|
|
for curr < len(bufBytes) {
|
|
var delim int
|
|
// read until the next newline or the end of the string
|
|
relDelim := bytes.IndexRune(bufBytes[curr:], '\n')
|
|
if relDelim != -1 {
|
|
count += 1 // new line found, add it to the count
|
|
delim = curr + relDelim
|
|
} else {
|
|
delim = len(bufBytes) // no new line found, read rest of text
|
|
}
|
|
|
|
str := string(bufBytes[curr:delim])
|
|
if lineWidth := terminal.StringWidth(str); lineWidth > w {
|
|
// account for word wrapping
|
|
count += lineWidth / w
|
|
if (lineWidth % w) == 0 {
|
|
// content whose width is exactly a multiplier of available width should not
|
|
// count as having wrapped on the last line
|
|
count -= 1
|
|
}
|
|
}
|
|
curr = delim + 1
|
|
}
|
|
|
|
return count
|
|
}
|
|
|