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