diff --git a/multiline.go b/multiline.go index bff9622f..a6586680 100644 --- a/multiline.go +++ b/multiline.go @@ -2,8 +2,6 @@ package survey import ( "strings" - - "github.com/AlecAivazis/survey/v2/terminal" ) type Multiline struct { @@ -28,12 +26,12 @@ var MultilineQuestionTemplate = ` {{- color .Config.Icons.Question.Format }}{{ .Config.Icons.Question.Text }} {{color "reset"}} {{- color "default+hb"}}{{ .Message }} {{color "reset"}} {{- if .ShowAnswer}} - {{- "\n"}}{{color "cyan"}}{{.Answer}}{{color "reset"}} - {{- if .Answer }}{{ "\n" }}{{ end }} + {{- if .Answer}}{{"\n"}}{{color "cyan"}}{{.Answer}}{{color "reset"}}{{end}} {{- else }} {{- if .Default}}{{color "white"}}({{.Default}}) {{color "reset"}}{{end}} {{- color "cyan"}}[Enter 2 empty lines to finish]{{color "reset"}} -{{- end}}` +{{- end}} +` func (i *Multiline) Prompt(config *PromptConfig) (interface{}, error) { // render the template @@ -70,13 +68,6 @@ func (i *Multiline) Prompt(config *PromptConfig) (interface{}, error) { if string(line) == "" { if emptyOnce { - numLines := len(multiline) + 2 - cursor.PreviousLine(numLines) - for j := 0; j < numLines; j++ { - terminal.EraseLine(i.Stdio().Out, terminal.ERASE_LINE_ALL) - cursor.NextLine(1) - } - cursor.PreviousLine(numLines) break } emptyOnce = true @@ -86,17 +77,19 @@ func (i *Multiline) Prompt(config *PromptConfig) (interface{}, error) { multiline = append(multiline, string(line)) } - val := strings.Join(multiline, "\n") - val = strings.TrimSpace(val) + // adjust for terminating newlines + cursor.PreviousLine(2) - // if the line is empty + // render the displayed value or use the default + val := strings.Join(multiline, "\n") if len(val) == 0 { - // use the default value return i.Default, err } - i.AppendRenderedText(val) - return val, err + + // remove the extra newline from the answer + ans := strings.TrimSuffix(val, "\n") + return ans, err } func (i *Multiline) Cleanup(config *PromptConfig, val interface{}) error { diff --git a/renderer.go b/renderer.go index a16207de..9cea98bb 100644 --- a/renderer.go +++ b/renderer.go @@ -42,14 +42,7 @@ func (r *Renderer) NewCursor() *terminal.Cursor { } 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() - + // create a formatted and plain error template with data userOut, layoutOut, err := core.RunTemplate(ErrorTemplate, &ErrorTemplateData{ Error: invalid, Icon: config.Icons.Error, @@ -58,7 +51,14 @@ func (r *Renderer) Error(config *PromptConfig, invalid error) error { return err } - // send the message to the user + // erase the currently rendered error and prompt + r.resetPrompt(r.countLines(r.renderedErrors)) + r.renderedErrors.Reset() + + r.resetPrompt(r.countLines(r.renderedText)) + r.renderedText.Reset() + + // print the formatted prompt if _, err := fmt.Fprint(terminal.NewAnsiStdout(r.stdio.Out), userOut); err != nil { return err } @@ -78,18 +78,17 @@ func (r *Renderer) OffsetCursor(offset int) { } 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 + // create a formatted and plain template with data userOut, layoutOut, err := core.RunTemplate(tmpl, data) if err != nil { return err } - // print the summary + // erase the currently rendered prompt + r.resetPrompt(r.countLines(r.renderedText)) + r.renderedText.Reset() + + // print the formatted prompt if _, err := fmt.Fprint(terminal.NewAnsiStdout(r.stdio.Out), userOut); err != nil { return err } @@ -130,16 +129,11 @@ func (r *Renderer) AppendRenderedText(text string) { r.renderedText.WriteString(text) } +// resetPrompt clears the previous lines of the past prompt 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) - } + cursor.PreviousLine(lines) + terminal.EraseScreen(r.stdio.Out, terminal.ERASE_SCREEN_END) } func (r *Renderer) termWidth() (int, error) { @@ -161,8 +155,7 @@ func (r *Renderer) termWidthSafe() int { // 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() - + termWidth := r.termWidthSafe() bufBytes := buf.Bytes() count := 0 @@ -179,10 +172,11 @@ func (r *Renderer) countLines(buf bytes.Buffer) int { } str := string(bufBytes[curr:delim]) - if lineWidth := terminal.StringWidth(str); lineWidth > w { + lineWidth := terminal.StringWidth(str) + if lineWidth > termWidth { // account for word wrapping - count += lineWidth / w - if (lineWidth % w) == 0 { + count += lineWidth / termWidth + if (lineWidth % termWidth) == 0 { // content whose width is exactly a multiplier of available width should not // count as having wrapped on the last line count -= 1 diff --git a/terminal/cursor.go b/terminal/cursor.go index 75117e08..1b216466 100644 --- a/terminal/cursor.go +++ b/terminal/cursor.go @@ -47,18 +47,24 @@ func (c *Cursor) Back(n int) error { // NextLine moves cursor to beginning of the line n lines down. func (c *Cursor) NextLine(n int) error { - if err := c.Down(1); err != nil { + if err := c.HorizontalAbsolute(0); err != nil { return err } - return c.HorizontalAbsolute(0) + if n == 0 { + return nil + } + return c.Down(n) } // PreviousLine moves cursor to beginning of the line n lines up. func (c *Cursor) PreviousLine(n int) error { - if err := c.Up(1); err != nil { + if err := c.HorizontalAbsolute(0); err != nil { return err } - return c.HorizontalAbsolute(0) + if n == 0 { + return nil + } + return c.Up(n) } // HorizontalAbsolute moves cursor horizontally to x. diff --git a/terminal/display.go b/terminal/display.go index 0f014b13..a1218e14 100644 --- a/terminal/display.go +++ b/terminal/display.go @@ -1,9 +1,16 @@ package terminal type EraseLineMode int +type EraseScreenMode int const ( ERASE_LINE_END EraseLineMode = iota ERASE_LINE_START ERASE_LINE_ALL ) + +const ( + ERASE_SCREEN_END EraseScreenMode = iota + ERASE_SCREEN_START + ERASE_SCREEN_ALL +) diff --git a/terminal/display_posix.go b/terminal/display_posix.go index fbd1b794..304af489 100644 --- a/terminal/display_posix.go +++ b/terminal/display_posix.go @@ -11,3 +11,8 @@ func EraseLine(out FileWriter, mode EraseLineMode) error { _, err := fmt.Fprintf(out, "\x1b[%dK", mode) return err } + +func EraseScreen(out FileWriter, mode EraseScreenMode) error { + _, err := fmt.Fprintf(out, "\x1b[%dJ", mode) + return err +} diff --git a/terminal/display_windows.go b/terminal/display_windows.go index fc9db9f7..8306f4d0 100644 --- a/terminal/display_windows.go +++ b/terminal/display_windows.go @@ -29,3 +29,22 @@ func EraseLine(out FileWriter, mode EraseLineMode) error { _, _, err := procFillConsoleOutputCharacter.Call(uintptr(handle), uintptr(' '), uintptr(x), uintptr(*(*int32)(unsafe.Pointer(&cursor))), uintptr(unsafe.Pointer(&w))) return normalizeError(err) } + +func EraseScreen(out FileWriter, mode EraseScreenMode) error { + handle := syscall.Handle(out.Fd()) + + var csbi consoleScreenBufferInfo + if _, _, err := procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))); normalizeError(err) != nil { + return err + } + + var w uint32 + cursor := csbi.cursorPosition + + lineCount := csbi.window.bottom - csbi.cursorPosition.Y + termWidth := csbi.size.X + screenSize := lineCount * termWidth + + _, _, err := procFillConsoleOutputCharacter.Call(uintptr(handle), uintptr(' '), uintptr(screenSize), uintptr(*(*int32)(unsafe.Pointer(&cursor))), uintptr(unsafe.Pointer(&w))) + return normalizeError(err) +}