1package survey 2 3import ( 4 "bytes" 5 "fmt" 6 "unicode/utf8" 7 8 "github.com/AlecAivazis/survey/v2/core" 9 "github.com/AlecAivazis/survey/v2/terminal" 10 goterm "golang.org/x/crypto/ssh/terminal" 11) 12 13type Renderer struct { 14 stdio terminal.Stdio 15 renderedErrors bytes.Buffer 16 renderedText bytes.Buffer 17} 18 19type ErrorTemplateData struct { 20 Error error 21 Icon Icon 22} 23 24var ErrorTemplate = `{{color .Icon.Format }}{{ .Icon.Text }} Sorry, your reply was invalid: {{ .Error.Error }}{{color "reset"}} 25` 26 27func (r *Renderer) WithStdio(stdio terminal.Stdio) { 28 r.stdio = stdio 29} 30 31func (r *Renderer) Stdio() terminal.Stdio { 32 return r.stdio 33} 34 35func (r *Renderer) NewRuneReader() *terminal.RuneReader { 36 return terminal.NewRuneReader(r.stdio) 37} 38 39func (r *Renderer) NewCursor() *terminal.Cursor { 40 return &terminal.Cursor{ 41 In: r.stdio.In, 42 Out: r.stdio.Out, 43 } 44} 45 46func (r *Renderer) Error(config *PromptConfig, invalid error) error { 47 // cleanup the currently rendered errors 48 r.resetPrompt(r.countLines(r.renderedErrors)) 49 r.renderedErrors.Reset() 50 51 // cleanup the rest of the prompt 52 r.resetPrompt(r.countLines(r.renderedText)) 53 r.renderedText.Reset() 54 55 userOut, layoutOut, err := core.RunTemplate(ErrorTemplate, &ErrorTemplateData{ 56 Error: invalid, 57 Icon: config.Icons.Error, 58 }) 59 if err != nil { 60 return err 61 } 62 63 // send the message to the user 64 fmt.Fprint(terminal.NewAnsiStdout(r.stdio.Out), userOut) 65 66 // add the printed text to the rendered error buffer so we can cleanup later 67 r.appendRenderedError(layoutOut) 68 69 return nil 70} 71 72func (r *Renderer) Render(tmpl string, data interface{}) error { 73 // cleanup the currently rendered text 74 lineCount := r.countLines(r.renderedText) 75 r.resetPrompt(lineCount) 76 r.renderedText.Reset() 77 78 // render the template summarizing the current state 79 userOut, layoutOut, err := core.RunTemplate(tmpl, data) 80 if err != nil { 81 return err 82 } 83 84 // print the summary 85 fmt.Fprint(terminal.NewAnsiStdout(r.stdio.Out), userOut) 86 87 // add the printed text to the rendered text buffer so we can cleanup later 88 r.AppendRenderedText(layoutOut) 89 90 // nothing went wrong 91 return nil 92} 93 94// appendRenderedError appends text to the renderer's error buffer 95// which is used to track what has been printed. It is not exported 96// as errors should only be displayed via Error(config, error). 97func (r *Renderer) appendRenderedError(text string) { 98 r.renderedErrors.WriteString(text) 99} 100 101// AppendRenderedText appends text to the renderer's text buffer 102// which is used to track of what has been printed. The buffer is used 103// to calculate how many lines to erase before updating the prompt. 104func (r *Renderer) AppendRenderedText(text string) { 105 r.renderedText.WriteString(text) 106} 107 108func (r *Renderer) resetPrompt(lines int) { 109 // clean out current line in case tmpl didnt end in newline 110 cursor := r.NewCursor() 111 cursor.HorizontalAbsolute(0) 112 terminal.EraseLine(r.stdio.Out, terminal.ERASE_LINE_ALL) 113 // clean up what we left behind last time 114 for i := 0; i < lines; i++ { 115 cursor.PreviousLine(1) 116 terminal.EraseLine(r.stdio.Out, terminal.ERASE_LINE_ALL) 117 } 118} 119 120func (r *Renderer) termWidth() (int, error) { 121 fd := int(r.stdio.Out.Fd()) 122 termWidth, _, err := goterm.GetSize(fd) 123 return termWidth, err 124} 125 126// countLines will return the count of `\n` with the addition of any 127// lines that have wrapped due to narrow terminal width 128func (r *Renderer) countLines(buf bytes.Buffer) int { 129 w, err := r.termWidth() 130 if err != nil || w == 0 { 131 // if we got an error due to terminal.GetSize not being supported 132 // on current platform then just assume a very wide terminal 133 w = 10000 134 } 135 136 bufBytes := buf.Bytes() 137 138 count := 0 139 curr := 0 140 delim := -1 141 for curr < len(bufBytes) { 142 // read until the next newline or the end of the string 143 relDelim := bytes.IndexRune(bufBytes[curr:], '\n') 144 if relDelim != -1 { 145 count += 1 // new line found, add it to the count 146 delim = curr + relDelim 147 } else { 148 delim = len(bufBytes) // no new line found, read rest of text 149 } 150 151 // account for word wrapping 152 count += int(utf8.RuneCount(bufBytes[curr:delim]) / w) 153 curr = delim + 1 154 } 155 156 return count 157} 158