forked from toolshed/abra
		
	
		
			
				
	
	
		
			196 lines
		
	
	
		
			4.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			196 lines
		
	
	
		
			4.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package survey
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"fmt"
 | |
| 	"github.com/AlecAivazis/survey/v2/core"
 | |
| 	"github.com/AlecAivazis/survey/v2/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
 | |
| }
 |