Skip to content
This repository has been archived by the owner on Apr 19, 2024. It is now read-only.

Clear to end of screen before redrawing prompt #476

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
29 changes: 11 additions & 18 deletions multiline.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ package survey

import (
"strings"

"github.com/AlecAivazis/survey/v2/terminal"
)

type Multiline struct {
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand Down
52 changes: 23 additions & 29 deletions renderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
}
Expand All @@ -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
}
Expand Down Expand Up @@ -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) {
Expand All @@ -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
Expand All @@ -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
Expand Down
14 changes: 10 additions & 4 deletions terminal/cursor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why first move horizontal and then try to move between lines?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There seems to be weird behavior when going Down(0) or Up(0), where the cursor moves down or up 1 instead of remaining on the same line. I'm not sure if this is expected ANSI behavior for \x1b[0A, but this change was made to guard against this case.

It might be more appropriate to move this check for n == 0 to the Down and Up functions instead? I wasn't sure if this current behavior was relied on by other functions and felt that this change was safer.

return err
}
return c.HorizontalAbsolute(0)
if n == 0 {
return nil
}
return c.Down(n)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch that this didn't use to forward the n parameter and just moved down 1 line each time 👍

}

// 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.
Expand Down
7 changes: 7 additions & 0 deletions terminal/display.go
Original file line number Diff line number Diff line change
@@ -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
)
5 changes: 5 additions & 0 deletions terminal/display_posix.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
19 changes: 19 additions & 0 deletions terminal/display_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +44 to +46
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Windows version of this EraseScreen function clears the lines from the cursor (at the beginning of the prompt) to the end of the displayed window (which is the end of the outputted prompt). For reference, the consoleScreenBufferInfo structure contains terminal dimensions and coordinates of the window and cursor.

This seems to match the behavior of the POSIX EraseScreen for resetting the prompt, but doesn't account for the EraseScreenMode. Not sure if supporting the other modes (clearing to the beginning of the screen and the entire screen) is necessary for how this function is being used, but I'd be in favor of forgoing them to keep this implementation more simple. Definitely open to thoughts here!


_, _, err := procFillConsoleOutputCharacter.Call(uintptr(handle), uintptr(' '), uintptr(screenSize), uintptr(*(*int32)(unsafe.Pointer(&cursor))), uintptr(unsafe.Pointer(&w)))
return normalizeError(err)
}