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

Fix redrawing prompts on Windows #474

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
41 changes: 41 additions & 0 deletions internal/log/log.go
@@ -0,0 +1,41 @@
package log

import (
"fmt"
"log"
"os"
"strings"
)

var enabled bool

func init() {
logFile := os.Getenv("SURVEY_LOG_FILE")
if logFile == "" {
return
}

w, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
fmt.Fprintf(os.Stderr, "survey: enable to open file %q for logging\n", logFile)
return
}

log.SetOutput(w)
log.Print(strings.Repeat("=", 80))
enabled = true
}

func Print(s string) {
if !enabled {
return
}
log.Print(s)
}

func Printf(f string, args ...interface{}) {
if !enabled {
return
}
log.Printf(f, args...)
}
8 changes: 8 additions & 0 deletions renderer.go
Expand Up @@ -3,7 +3,9 @@ package survey
import (
"bytes"
"fmt"

"github.com/AlecAivazis/survey/v2/core"
"github.com/AlecAivazis/survey/v2/internal/log"
"github.com/AlecAivazis/survey/v2/terminal"
"golang.org/x/term"
)
Expand Down Expand Up @@ -58,6 +60,7 @@ func (r *Renderer) Error(config *PromptConfig, invalid error) error {
return err
}

log.Printf("Error() %q", userOut)
// send the message to the user
if _, err := fmt.Fprint(terminal.NewAnsiStdout(r.stdio.Out), userOut); err != nil {
return err
Expand Down Expand Up @@ -89,6 +92,7 @@ func (r *Renderer) Render(tmpl string, data interface{}) error {
return err
}

log.Printf("Render() %q", userOut)
// print the summary
if _, err := fmt.Fprint(terminal.NewAnsiStdout(r.stdio.Out), userOut); err != nil {
return err
Expand Down Expand Up @@ -120,23 +124,27 @@ func (r *Renderer) RenderWithCursorOffset(tmpl string, data IterableOpts, opts [
// which is used to track what has been printed. It is not exported
// as errors should only be displayed via Error(config, error).
func (r *Renderer) appendRenderedError(text string) {
log.Printf("appendRenderedError: %q", text)
r.renderedErrors.WriteString(text)
}

// AppendRenderedText appends text to the renderer's text buffer
// which is used to track of what has been printed. The buffer is used
// to calculate how many lines to erase before updating the prompt.
func (r *Renderer) AppendRenderedText(text string) {
log.Printf("AppendRenderedText: %q", text)
r.renderedText.WriteString(text)
}

func (r *Renderer) resetPrompt(lines int) {
// clean out current line in case tmpl didnt end in newline
cursor := r.NewCursor()
cursor.HorizontalAbsolute(0)
log.Print("resetPrompt: erasing current line")
terminal.EraseLine(r.stdio.Out, terminal.ERASE_LINE_ALL)
// clean up what we left behind last time
for i := 0; i < lines; i++ {
log.Print("resetPrompt: moving one line up and erasing line")
cursor.PreviousLine(1)
terminal.EraseLine(r.stdio.Out, terminal.ERASE_LINE_ALL)
}
Expand Down
3 changes: 3 additions & 0 deletions survey.go
Expand Up @@ -9,6 +9,7 @@ import (
"unicode/utf8"

"github.com/AlecAivazis/survey/v2/core"
"github.com/AlecAivazis/survey/v2/internal/log"
"github.com/AlecAivazis/survey/v2/terminal"
)

Expand Down Expand Up @@ -363,6 +364,7 @@ func Ask(qs []*Question, response interface{}, opts ...AskOpt) error {
if promptAgainer, ok := q.Prompt.(PromptAgainer); ok && validationErr != nil {
ans, err = promptAgainer.PromptAgain(&options.PromptConfig, ans, validationErr)
} else {
log.Printf("Prompt() field: %s", q.Name)
ans, err = q.Prompt.Prompt(&options.PromptConfig)
}
if err != nil {
Expand All @@ -384,6 +386,7 @@ func Ask(qs []*Question, response interface{}, opts ...AskOpt) error {
}

// tell the prompt to cleanup with the validated value
log.Printf("Prompt cleanup for %q", q.Name)
if err := q.Prompt.Cleanup(&options.PromptConfig, ans); err != nil {
return err
}
Expand Down
69 changes: 34 additions & 35 deletions terminal/cursor_windows.go
Expand Up @@ -2,8 +2,11 @@ package terminal

import (
"bytes"
"fmt"
"syscall"
"unsafe"

"github.com/AlecAivazis/survey/v2/internal/log"
)

var COORDINATE_SYSTEM_BEGIN Short = 0
Expand All @@ -17,19 +20,19 @@ type Cursor struct {
}

func (c *Cursor) Up(n int) error {
return c.cursorMove(0, n)
return c.cursorMove(0, -1*n, false)
}

func (c *Cursor) Down(n int) error {
return c.cursorMove(0, -1*n)
return c.cursorMove(0, n, false)
}

func (c *Cursor) Forward(n int) error {
return c.cursorMove(n, 0)
return c.cursorMove(n, 0, false)
}

func (c *Cursor) Back(n int) error {
return c.cursorMove(-1*n, 0)
return c.cursorMove(-1*n, 0, false)
}

// save the cursor location
Expand Down Expand Up @@ -57,60 +60,56 @@ func (cur Coord) CursorIsAtLineBegin() bool {
return cur.X == 0
}

func (c *Cursor) cursorMove(x int, y int) error {
func (c *Cursor) cursorMove(x int, y int, xIsAbs bool) error {
handle := syscall.Handle(c.Out.Fd())

var csbi consoleScreenBufferInfo
if _, _, err := procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))); normalizeError(err) != nil {
log.Printf("cursorMove READ ERROR: %v", err)
return err
}

var cursor Coord
cursor.X = csbi.cursorPosition.X + Short(x)
if xIsAbs {
cursor.X = Short(x)
} else {
cursor.X = csbi.cursorPosition.X + Short(x)
}
cursor.Y = csbi.cursorPosition.Y + Short(y)

xAbsLabel := ""
if xIsAbs {
xAbsLabel = " (abs)"
}
log.Printf("cursorMove X:%d%s Y:%d => %d, %d", x, xAbsLabel, y, cursor.X, cursor.Y)

_, _, err := procSetConsoleCursorPosition.Call(uintptr(handle), uintptr(*(*int32)(unsafe.Pointer(&cursor))))
return normalizeError(err)
if normalizeError(err) != nil {
log.Printf("cursorMove WRITE ERROR: %v", err)
return err
}
return nil
}

func (c *Cursor) NextLine(n int) error {
if err := c.Up(n); err != nil {
return err
}
return c.HorizontalAbsolute(0)
return c.cursorMove(0, n, true)
}

func (c *Cursor) PreviousLine(n int) error {
if err := c.Down(n); err != nil {
return err
}
return c.HorizontalAbsolute(0)
return c.cursorMove(0, -1*n, true)
}

// for comparability purposes between windows
// in windows we don't have to print out a new line
func (c *Cursor) MoveNextLine(cur *Coord, terminalSize *Coord) error {
return c.NextLine(1)
}

func (c *Cursor) HorizontalAbsolute(x int) error {
handle := syscall.Handle(c.Out.Fd())

var csbi consoleScreenBufferInfo
if _, _, err := procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))); normalizeError(err) != nil {
if err := c.cursorMove(0, 1, true); err != nil {
// moving to the next line will fail when at the bottom of the terminal viewport
_, err = fmt.Fprint(c.Out, "\n")
return err
}
return nil
}

var cursor Coord
cursor.X = Short(x)
cursor.Y = csbi.cursorPosition.Y

if csbi.size.X < cursor.X {
cursor.X = csbi.size.X
}

_, _, err := procSetConsoleCursorPosition.Call(uintptr(handle), uintptr(*(*int32)(unsafe.Pointer(&cursor))))
return normalizeError(err)
func (c *Cursor) HorizontalAbsolute(x int) error {
return c.cursorMove(0, 0, true)
}

func (c *Cursor) Show() error {
Expand Down
5 changes: 5 additions & 0 deletions terminal/display_windows.go
Expand Up @@ -3,6 +3,8 @@ package terminal
import (
"syscall"
"unsafe"

"github.com/AlecAivazis/survey/v2/internal/log"
)

func EraseLine(out FileWriter, mode EraseLineMode) error {
Expand All @@ -19,11 +21,14 @@ func EraseLine(out FileWriter, mode EraseLineMode) error {
switch mode {
case ERASE_LINE_END:
x = csbi.size.X
log.Printf("EraseLine() END: %d", x)
case ERASE_LINE_START:
x = 0
log.Printf("EraseLine() START: %d", x)
case ERASE_LINE_ALL:
cursor.X = 0
x = csbi.size.X
log.Printf("EraseLine() ALL: %d-%d", 0, x)
}

_, _, err := procFillConsoleOutputCharacter.Call(uintptr(handle), uintptr(' '), uintptr(x), uintptr(*(*int32)(unsafe.Pointer(&cursor))), uintptr(unsafe.Pointer(&w)))
Expand Down