From 338c15629094b417ec1ae52eb6dd6fe086a94656 Mon Sep 17 00:00:00 2001 From: Ethan Zimbelman Date: Fri, 2 Dec 2022 22:36:09 -0800 Subject: [PATCH 1/9] colocate erase and print prompt actions --- renderer.go | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/renderer.go b/renderer.go index a16207de..2cf13f3a 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 } From 9decb76170226706b12cb344d9ed02f159539e79 Mon Sep 17 00:00:00 2001 From: Ethan Zimbelman Date: Fri, 2 Dec 2022 22:37:21 -0800 Subject: [PATCH 2/9] rename w -> termWidth --- renderer.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/renderer.go b/renderer.go index 2cf13f3a..fb5d303f 100644 --- a/renderer.go +++ b/renderer.go @@ -160,8 +160,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 @@ -178,10 +177,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 From 363d3eb2925865085c17ef0ee46d1b86212fc70b Mon Sep 17 00:00:00 2001 From: Ethan Zimbelman Date: Fri, 2 Dec 2022 22:41:52 -0800 Subject: [PATCH 3/9] move n lines in NextLine and PreviousLine --- terminal/cursor.go | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) 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. From cf899544b65979aa197b0b38eb82f307b4e03f78 Mon Sep 17 00:00:00 2001 From: Ethan Zimbelman Date: Fri, 2 Dec 2022 22:44:41 -0800 Subject: [PATCH 4/9] erase to end of screen when resetting prompt --- renderer.go | 11 +++-------- terminal/display.go | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/renderer.go b/renderer.go index fb5d303f..9cea98bb 100644 --- a/renderer.go +++ b/renderer.go @@ -129,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) { diff --git a/terminal/display.go b/terminal/display.go index 0f014b13..9466ee2d 100644 --- a/terminal/display.go +++ b/terminal/display.go @@ -1,9 +1,25 @@ package terminal +import ( + "fmt" +) + 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 +) + +func EraseScreen(out FileWriter, mode EraseScreenMode) error { + _, err := fmt.Fprintf(out, "\x1b[%dJ", mode) + return err +} From 3090f9388592e6acf5235ad2012b7af18a4e82ab Mon Sep 17 00:00:00 2001 From: Ethan Zimbelman Date: Fri, 2 Dec 2022 22:47:58 -0800 Subject: [PATCH 5/9] preserve leading and trailing spaces --- multiline.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/multiline.go b/multiline.go index bff9622f..5ee37e5a 100644 --- a/multiline.go +++ b/multiline.go @@ -86,12 +86,12 @@ func (i *Multiline) Prompt(config *PromptConfig) (interface{}, error) { multiline = append(multiline, string(line)) } + // remove the trailing newline + multiline = multiline[:len(multiline)-1] val := strings.Join(multiline, "\n") - val = strings.TrimSpace(val) - // if the line is empty + // use the default value if the line is empty if len(val) == 0 { - // use the default value return i.Default, err } From 38e598ca818828f8515325044cc562ee1415336b Mon Sep 17 00:00:00 2001 From: Ethan Zimbelman Date: Fri, 2 Dec 2022 22:52:29 -0800 Subject: [PATCH 6/9] adjust cursor location after input for full reset --- multiline.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/multiline.go b/multiline.go index 5ee37e5a..4a8f2c96 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,6 +77,15 @@ func (i *Multiline) Prompt(config *PromptConfig) (interface{}, error) { multiline = append(multiline, string(line)) } + // position the cursor on last line of input + cursorPadding := 3 + + // ignore the empty line in an empty answer + if len(multiline) == 1 && multiline[0] == "" { + cursorPadding = 2 + } + cursor.PreviousLine(cursorPadding) + // remove the trailing newline multiline = multiline[:len(multiline)-1] val := strings.Join(multiline, "\n") From 9e539c369c2c3166b0a8c3e384ad9454f57acde4 Mon Sep 17 00:00:00 2001 From: Ethan Zimbelman Date: Mon, 5 Dec 2022 22:15:46 -0800 Subject: [PATCH 7/9] remove the extra newline only from the answer --- multiline.go | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/multiline.go b/multiline.go index 4a8f2c96..a6586680 100644 --- a/multiline.go +++ b/multiline.go @@ -77,26 +77,19 @@ func (i *Multiline) Prompt(config *PromptConfig) (interface{}, error) { multiline = append(multiline, string(line)) } - // position the cursor on last line of input - cursorPadding := 3 + // adjust for terminating newlines + cursor.PreviousLine(2) - // ignore the empty line in an empty answer - if len(multiline) == 1 && multiline[0] == "" { - cursorPadding = 2 - } - cursor.PreviousLine(cursorPadding) - - // remove the trailing newline - multiline = multiline[:len(multiline)-1] + // render the displayed value or use the default val := strings.Join(multiline, "\n") - - // use the default value if the line is empty if len(val) == 0 { 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 { From b96ec7a016d764375c2c6accf7b376c408cce6cd Mon Sep 17 00:00:00 2001 From: Ethan Zimbelman Date: Sat, 17 Dec 2022 17:18:07 -0800 Subject: [PATCH 8/9] clear the screen on a windows machine --- terminal/display.go | 9 --------- terminal/display_posix.go | 5 +++++ terminal/display_windows.go | 16 ++++++++++++++++ 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/terminal/display.go b/terminal/display.go index 9466ee2d..a1218e14 100644 --- a/terminal/display.go +++ b/terminal/display.go @@ -1,9 +1,5 @@ package terminal -import ( - "fmt" -) - type EraseLineMode int type EraseScreenMode int @@ -18,8 +14,3 @@ const ( ERASE_SCREEN_START ERASE_SCREEN_ALL ) - -func EraseScreen(out FileWriter, mode EraseScreenMode) error { - _, err := fmt.Fprintf(out, "\x1b[%dJ", mode) - return err -} 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..1f4ef11e 100644 --- a/terminal/display_windows.go +++ b/terminal/display_windows.go @@ -29,3 +29,19 @@ 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 + termSize := uint32(csbi.size.X) * uint32(csbi.size.Y) + cursor := csbi.cursorPosition + + _, _, err := procFillConsoleOutputCharacter.Call(uintptr(handle), uintptr(' '), uintptr(termSize), uintptr(*(*int32)(unsafe.Pointer(&cursor))), uintptr(unsafe.Pointer(&w))) + return err +} From d29d6fa9a7a31b354b33c29b6128b504fd3c6af6 Mon Sep 17 00:00:00 2001 From: Ethan Zimbelman Date: Thu, 22 Dec 2022 19:03:07 -0800 Subject: [PATCH 9/9] clear from cursor to end of window on windows --- terminal/display_windows.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/terminal/display_windows.go b/terminal/display_windows.go index 1f4ef11e..8306f4d0 100644 --- a/terminal/display_windows.go +++ b/terminal/display_windows.go @@ -39,9 +39,12 @@ func EraseScreen(out FileWriter, mode EraseScreenMode) error { } var w uint32 - termSize := uint32(csbi.size.X) * uint32(csbi.size.Y) cursor := csbi.cursorPosition - _, _, err := procFillConsoleOutputCharacter.Call(uintptr(handle), uintptr(' '), uintptr(termSize), uintptr(*(*int32)(unsafe.Pointer(&cursor))), uintptr(unsafe.Pointer(&w))) - return err + 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) }