Repository: AlecAivazis/survey
Pull request #309 (2020-11-06 11:02:55)

Fix PreviousLine and NextLine on linux
References:

#300 #302 #307 #308 

I think this change will be enough to make it work on both Windows and Linux. I have tested in cmd.exe on windows, and on Ubuntu Konsole and VSCode integrated terminal. **I cannot test on MacOS though because I don't own a Mac**.

**Logic behind this change**:

In `cursor_windows.go`, `PreviousLine()` calls `Down()` and `HorizontalAbsolute()`, so I just did the same for linux but without `HorizontalAbsolute()` because it is apparently not needed (Test it on MacOS and tell me if it works. If it doesn't, then I think adding a call to `HorizontalAbsolute()` would do the trick). This allows us to keep `PreviousLine()` and `NextLine()` and don't have a conflict in naming between the windows and other versions.


Patch:
diff --git a/input.go b/input.go
index f34177f..a544b04 100644
--- a/input.go
+++ b/input.go
@@ -190,12 +190,20 @@ func (i *Input) Prompt(config *PromptConfig) (interface{}, error) {
 }
 
 func (i *Input) Cleanup(config *PromptConfig, val interface{}) error {
+	// use the default answer when cleaning up the prompt if necessary
+	ans := i.answer
+	if ans == "" && i.Default != "" {
+		ans = i.Default
+	}
+
+	// render the cleanup
 	return i.Render(
 		InputQuestionTemplate,
 		InputTemplateData{
 			Input:      *i,
 			ShowAnswer: true,
 			Config:     config,
+			Answer:     ans,
 		},
 	)
 }
diff --git a/multiselect.go b/multiselect.go
index 0e43477..628abd7 100644
--- a/multiselect.go
+++ b/multiselect.go
@@ -273,7 +273,10 @@ func (m *MultiSelect) Prompt(config *PromptConfig) (interface{}, error) {
 
 	// start waiting for input
 	for {
-		r, _, _ := rr.ReadRune()
+		r, _, err := rr.ReadRune()
+		if err != nil {
+			return "", err
+		}
 		if r == '\r' || r == '\n' {
 			break
 		}
diff --git a/survey.go b/survey.go
index e004cf3..b7ad8a0 100644
--- a/survey.go
+++ b/survey.go
@@ -10,6 +10,10 @@ import (
 	"github.com/AlecAivazis/survey/v2/terminal"
 )
 
+// OnInterrupt is the function to run when
+// SIGINT (CTRL+C) is sent to the process.
+var OnInterrupt func()
+
 // DefaultAskOptions is the default options on ask, using the OS stdio.
 func defaultAskOptions() *AskOptions {
 	return &AskOptions{
@@ -56,6 +60,7 @@ func defaultAskOptions() *AskOptions {
 			},
 			KeepFilter: false,
 		},
+		OnInterrupt: OnInterrupt,
 	}
 }
 func defaultPromptConfig() *PromptConfig {
@@ -137,6 +142,7 @@ type AskOptions struct {
 	Stdio        terminal.Stdio
 	Validators   []Validator
 	PromptConfig PromptConfig
+	OnInterrupt  func()
 }
 
 // WithStdio specifies the standard input, output and error files survey
@@ -182,6 +188,16 @@ func WithValidator(v Validator) AskOpt {
 	}
 }
 
+// WithInterruptFunc specifies a function to run on recieving
+// SIGINT (aka CTRL+C) during prompt.
+func WithInterruptFunc(fn func()) AskOpt {
+	return func(options *AskOptions) error {
+		options.OnInterrupt = fn
+		// nothing went wrong
+		return nil
+	}
+}
+
 type wantsStdio interface {
 	WithStdio(terminal.Stdio)
 }
@@ -291,6 +307,10 @@ func Ask(qs []*Question, response interface{}, opts ...AskOpt) error {
 
 		// grab the user input and save it
 		ans, err := q.Prompt.Prompt(&options.PromptConfig)
+		// if SIGINT is recieved.
+		if err == terminal.InterruptErr {
+			options.OnInterrupt()
+		}
 		// if there was a problem
 		if err != nil {
 			return err
diff --git a/terminal/cursor.go b/terminal/cursor.go
index c5b6841..c294ca4 100644
--- a/terminal/cursor.go
+++ b/terminal/cursor.go
@@ -42,12 +42,12 @@ func (c *Cursor) Back(n int) {
 
 // NextLine moves cursor to beginning of the line n lines down.
 func (c *Cursor) NextLine(n int) {
-	fmt.Fprintf(c.Out, "\x1b[%dE", n)
+	c.Down(1)
 }
 
 // PreviousLine moves cursor to beginning of the line n lines up.
 func (c *Cursor) PreviousLine(n int) {
-	fmt.Fprintf(c.Out, "\x1b[%dF", n)
+	c.Up(1)
 }
 
 // HorizontalAbsolute moves cursor horizontally to x.


Test patch:
diff --git a/survey_posix_test.go b/survey_posix_test.go
index 95dea35..c6d5de1 100644
--- a/survey_posix_test.go
+++ b/survey_posix_test.go
@@ -12,7 +12,7 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
-func RunTest(t *testing.T, procedure func(*expect.Console), test func(terminal.Stdio) error) {
+func RunTest(t *testing.T, procedure func(*expect.Console), test func(terminal.Stdio) error) error {
 	t.Parallel()
 
 	// Multiplex output to a buffer as well for the raw bytes.
@@ -28,7 +28,6 @@ func RunTest(t *testing.T, procedure func(*expect.Console), test func(terminal.S
 	}()
 
 	err = test(Stdio(c))
-	require.Nil(t, err)
 
 	// Close the slave end of the pty, and read the remaining bytes from the master end.
 	c.Tty().Close()
@@ -38,4 +37,6 @@ func RunTest(t *testing.T, procedure func(*expect.Console), test func(terminal.S
 
 	// Dump the terminal's screen.
 	t.Logf("\n%s", expect.StripTrailingEmptyLines(state.String()))
+
+	return err
 }
diff --git a/survey_test.go b/survey_test.go
index 58f8420..6a222df 100644
--- a/survey_test.go
+++ b/survey_test.go
@@ -31,7 +31,7 @@ type PromptTest struct {
 
 func RunPromptTest(t *testing.T, test PromptTest) {
 	var answer interface{}
-	RunTest(t, test.procedure, func(stdio terminal.Stdio) error {
+	err := RunTest(t, test.procedure, func(stdio terminal.Stdio) error {
 		var err error
 		if p, ok := test.prompt.(wantsStdio); ok {
 			p.WithStdio(stdio)
@@ -40,12 +40,13 @@ func RunPromptTest(t *testing.T, test PromptTest) {
 		answer, err = test.prompt.Prompt(defaultPromptConfig())
 		return err
 	})
+	require.Nil(t, err)
 	require.Equal(t, test.expected, answer)
 }
 
 func RunPromptTestKeepFilter(t *testing.T, test PromptTest) {
 	var answer interface{}
-	RunTest(t, test.procedure, func(stdio terminal.Stdio) error {
+	err := RunTest(t, test.procedure, func(stdio terminal.Stdio) error {
 		var err error
 		if p, ok := test.prompt.(wantsStdio); ok {
 			p.WithStdio(stdio)
@@ -55,6 +56,7 @@ func RunPromptTestKeepFilter(t *testing.T, test PromptTest) {
 		answer, err = test.prompt.Prompt(config)
 		return err
 	})
+	require.Nil(t, err)
 	require.Equal(t, test.expected, answer)
 }
 
@@ -133,7 +135,6 @@ func TestPagination_lastHalf(t *testing.T) {
 
 func TestAsk(t *testing.T) {
 	t.Skip()
-	return
 	tests := []struct {
 		name      string
 		questions []*Question
@@ -250,10 +251,10 @@ func TestAsk(t *testing.T) {
 				"pizza":                    true,
 				"commit-message":           "Add editor prompt tests\n",
 				"commit-message-validated": "Add editor prompt tests\n",
-				"name":     "Johnny Appleseed",
-				"day":      []string{"Monday", "Wednesday"},
-				"password": "secret",
-				"color":    "yellow",
+				"name":                     "Johnny Appleseed",
+				"day":                      []string{"Monday", "Wednesday"},
+				"password":                 "secret",
+				"color":                    "yellow",
 			},
 		},
 		{
@@ -305,9 +306,10 @@ func TestAsk(t *testing.T) {
 		test := test
 		t.Run(test.name, func(t *testing.T) {
 			answers := make(map[string]interface{})
-			RunTest(t, test.procedure, func(stdio terminal.Stdio) error {
+			err := RunTest(t, test.procedure, func(stdio terminal.Stdio) error {
 				return Ask(test.questions, &answers, WithStdio(stdio.In, stdio.Out, stdio.Err))
 			})
+			require.Nil(t, err)
 			require.Equal(t, test.expected, answers)
 		})
 	}
@@ -323,3 +325,57 @@ func TestAsk_returnsErrorIfTargetIsNil(t *testing.T) {
 		t.Error("Did not encounter error when asking with no where to record.")
 	}
 }
+
+func TestOnInterruptFunc(t *testing.T) {
+	// No Interrupt function set.
+	t.Run("No OnInterrupt", func(t *testing.T) {
+		answer := ""
+		err := RunTest(t, func(e *expect.Console) {
+			e.ExpectString("Are you a bot?")
+			e.SendLine(string(terminal.KeyInterrupt))
+			e.ExpectEOF()
+		}, func(t terminal.Stdio) error {
+			return AskOne(&Input{Message: "Are you a bot?"}, &answer,
+				WithStdio(t.In, t.Out, t.Err))
+		})
+
+		require.Equal(t, terminal.InterruptErr, err)
+		require.Equal(t, "", answer)
+	})
+
+	// Set global Interrupt function.
+	OnInterrupt = func() { fmt.Println("Ended abruptly!") }
+	t.Run("Global OnInterrupt", func(t *testing.T) {
+		answer := ""
+		err := RunTest(t, func(e *expect.Console) {
+			e.ExpectString("Are you a bot?")
+			e.SendLine(string(terminal.KeyInterrupt))
+			e.ExpectString("Ended abruptly!")
+			e.ExpectEOF()
+		}, func(t terminal.Stdio) error {
+			return AskOne(&Input{Message: "Are you a bot?"}, &answer,
+				WithStdio(t.In, t.Out, t.Err))
+		})
+
+		require.Equal(t, terminal.InterruptErr, err)
+		require.Equal(t, "", answer)
+	})
+
+	// Set local Interrupt function (overide global).
+	t.Run("Local Interrupt (override global", func(t *testing.T) {
+		answer := ""
+		err := RunTest(t, func(e *expect.Console) {
+			e.ExpectString("Are you a bot?")
+			e.SendLine(string(terminal.KeyInterrupt))
+			e.ExpectString("The end.")
+			e.ExpectEOF()
+		}, func(t terminal.Stdio) error {
+			return AskOne(&Input{Message: "Are you a bot?"}, &answer,
+				WithStdio(t.In, t.Out, t.Err),
+				WithInterruptFunc(func() { fmt.Println("The end.") }))
+		})
+
+		require.Equal(t, terminal.InterruptErr, err)
+		require.Equal(t, "", answer)
+	})
+}
diff --git a/survey_windows_test.go b/survey_windows_test.go
index e22022c..3c26ad0 100644
--- a/survey_windows_test.go
+++ b/survey_windows_test.go
@@ -7,6 +7,7 @@ import (
 	expect "github.com/Netflix/go-expect"
 )
 
-func RunTest(t *testing.T, procedure func(*expect.Console), test func(terminal.Stdio) error) {
+func RunTest(t *testing.T, procedure func(*expect.Console), test func(terminal.Stdio) error) error {
 	t.Skip("Windows does not support psuedoterminals")
+	return nil
 }