Repository: pterm/pterm
Pull request #507 (2023-05-15 13:11:27)

Add OnExit function for interactive text inputs
### Description

For interactive programs a Ctrl-C might immediatly trigger an `os.Exit()` in the current implementation which will not allow for cleanup functions or defer code to run.

To avoid this a dedicated "OnExit" function can be configured using `OnExitFunc` or using `.OnExit()` when calling an interactive text input.

Naming can be adjusted of course.

### Scope
> What is affected by this pull request?

- [ ] Bug Fix
- [x] New Feature
- [ ] Documentation
- [ ] Other

### Related Issue
none yet

### To-Do Checklist
- [x] I tested my changes
- [x] I have commented every method that I created/changed
- [ ] I updated the examples to fit with my changes
- [x] I have added tests for my newly created methods


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/pterm/pterm/pull/507?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pterm) Report
> Merging [#507](https://app.codecov.io/gh/pterm/pterm/pull/507?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pterm) (e9bb62c) into [master](https://app.codecov.io/gh/pterm/pterm/commit/587c66eeb27753340645973c6fadd4971d332bf9?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pterm) (587c66e) will **decrease** coverage by `0.29%`.
> The diff coverage is `n/a`.

```diff
@@            Coverage Diff             @@
##           master     #507      +/-   ##
==========================================
- Coverage   82.23%   81.95%   -0.29%     
==========================================
  Files          30       30              
  Lines        3484     3496      +12     
==========================================
  Hits         2865     2865              
- Misses        562      574      +12     
  Partials       57       57              
```


| [Impacted Files](https://app.codecov.io/gh/pterm/pterm/pull/507?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pterm) | Coverage Δ | |
|---|---|---|
| [interactive\_confirm\_printer.go](https://app.codecov.io/gh/pterm/pterm/pull/507?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pterm#diff-aW50ZXJhY3RpdmVfY29uZmlybV9wcmludGVyLmdv) | `89.13% <ø> (-3.01%)` | :arrow_down: |
| [interactive\_multiselect\_printer.go](https://app.codecov.io/gh/pterm/pterm/pull/507?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pterm#diff-aW50ZXJhY3RpdmVfbXVsdGlzZWxlY3RfcHJpbnRlci5nbw==) | `55.09% <ø> (-0.64%)` | :arrow_down: |
| [interactive\_select\_printer.go](https://app.codecov.io/gh/pterm/pterm/pull/507?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pterm#diff-aW50ZXJhY3RpdmVfc2VsZWN0X3ByaW50ZXIuZ28=) | `52.45% <ø> (-0.79%)` | :arrow_down: |
| [interactive\_textinput\_printer.go](https://app.codecov.io/gh/pterm/pterm/pull/507?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pterm#diff-aW50ZXJhY3RpdmVfdGV4dGlucHV0X3ByaW50ZXIuZ28=) | `46.78% <ø> (-0.84%)` | :arrow_down: |


MarvinJWendt: Thanks! I'll take a look soon.
lammel: It seems like bug #509 describes quite a similar scenario.

Maybe it would be preferable to not exit by default but rather abort the interactive input. 
In that case we can adjust this PR or just drop it.
MarvinJWendt: Btw, you don't have to add tests, I am currently rewriting the whole testing system in #505.
lammel: > We can do this, and in combination add a `OnInterrupt` function (renaming the `OnExit`). So the developer can decide what to do next. I like the idea. Do you want to make those changes to this PR?

Yes, I'll give it a try in the next few days.
lammel: Renamed to `OnInterrupt()` to allow providing a function to execute in case input is canceled.
By default canceling the input with Ctrl-C will not exit anymore. This could also lead to unexpected behavior though 

Patch:
diff --git a/.golangci.yml b/.golangci.yml
index 57ba35d8..a964631f 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -28,7 +28,6 @@ linters:
     - gosec
     - govet
     - ineffassign
-    - interfacer
     - unconvert
     - gosimple
     - godox
diff --git a/interactive_confirm_printer.go b/interactive_confirm_printer.go
index 6760ce69..c5065919 100644
--- a/interactive_confirm_printer.go
+++ b/interactive_confirm_printer.go
@@ -28,14 +28,15 @@ var (
 
 // InteractiveConfirmPrinter is a printer for interactive confirm prompts.
 type InteractiveConfirmPrinter struct {
-	DefaultValue bool
-	DefaultText  string
-	TextStyle    *Style
-	ConfirmText  string
-	ConfirmStyle *Style
-	RejectText   string
-	RejectStyle  *Style
-	SuffixStyle  *Style
+	DefaultValue    bool
+	DefaultText     string
+	TextStyle       *Style
+	ConfirmText     string
+	ConfirmStyle    *Style
+	RejectText      string
+	RejectStyle     *Style
+	SuffixStyle     *Style
+	OnInterruptFunc func()
 }
 
 // WithDefaultText sets the default text.
@@ -86,6 +87,12 @@ func (p InteractiveConfirmPrinter) WithSuffixStyle(style *Style) *InteractiveCon
 	return &p
 }
 
+// OnInterrupt sets the function to execute on exit of the input reader
+func (p InteractiveConfirmPrinter) WithOnInterruptFunc(exitFunc func()) *InteractiveConfirmPrinter {
+	p.OnInterruptFunc = exitFunc
+	return &p
+}
+
 // Show shows the confirm prompt.
 //
 // Example:
@@ -95,7 +102,7 @@ func (p InteractiveConfirmPrinter) WithSuffixStyle(style *Style) *InteractiveCon
 func (p InteractiveConfirmPrinter) Show(text ...string) (bool, error) {
 	// should be the first defer statement to make sure it is executed last
 	// and all the needed cleanup can be done before
-	cancel, exit := internal.NewCancelationSignal()
+	cancel, exit := internal.NewCancelationSignal(p.OnInterruptFunc)
 	defer exit()
 
 	var result bool
diff --git a/interactive_multiselect_printer.go b/interactive_multiselect_printer.go
index 2f0b326c..1fa31844 100644
--- a/interactive_multiselect_printer.go
+++ b/interactive_multiselect_printer.go
@@ -32,16 +32,17 @@ var (
 
 // InteractiveMultiselectPrinter is a printer for interactive multiselect menus.
 type InteractiveMultiselectPrinter struct {
-	DefaultText    string
-	TextStyle      *Style
-	Options        []string
-	OptionStyle    *Style
-	DefaultOptions []string
-	MaxHeight      int
-	Selector       string
-	SelectorStyle  *Style
-	Filter         bool
-	Checkmark      *Checkmark
+	DefaultText     string
+	TextStyle       *Style
+	Options         []string
+	OptionStyle     *Style
+	DefaultOptions  []string
+	MaxHeight       int
+	Selector        string
+	SelectorStyle   *Style
+	Filter          bool
+	Checkmark       *Checkmark
+	OnInterruptFunc func()
 
 	selectedOption        int
 	selectedOptions       []int
@@ -109,11 +110,17 @@ func (p InteractiveMultiselectPrinter) WithCheckmark(checkmark *Checkmark) *Inte
 	return &p
 }
 
+// OnInterrupt sets the function to execute on exit of the input reader
+func (p InteractiveMultiselectPrinter) WithOnInterruptFunc(exitFunc func()) *InteractiveMultiselectPrinter {
+	p.OnInterruptFunc = exitFunc
+	return &p
+}
+
 // Show shows the interactive multiselect menu and returns the selected entry.
 func (p *InteractiveMultiselectPrinter) Show(text ...string) ([]string, error) {
 	// should be the first defer statement to make sure it is executed last
 	// and all the needed cleanup can be done before
-	cancel, exit := internal.NewCancelationSignal()
+	cancel, exit := internal.NewCancelationSignal(p.OnInterruptFunc)
 	defer exit()
 
 	if len(text) == 0 || Sprint(text[0]) == "" {
diff --git a/interactive_select_printer.go b/interactive_select_printer.go
index 0af1a0d6..22ac4efa 100644
--- a/interactive_select_printer.go
+++ b/interactive_select_printer.go
@@ -28,14 +28,15 @@ var (
 
 // InteractiveSelectPrinter is a printer for interactive select menus.
 type InteractiveSelectPrinter struct {
-	TextStyle     *Style
-	DefaultText   string
-	Options       []string
-	OptionStyle   *Style
-	DefaultOption string
-	MaxHeight     int
-	Selector      string
-	SelectorStyle *Style
+	TextStyle       *Style
+	DefaultText     string
+	Options         []string
+	OptionStyle     *Style
+	DefaultOption   string
+	MaxHeight       int
+	Selector        string
+	SelectorStyle   *Style
+	OnInterruptFunc func()
 
 	selectedOption        int
 	result                string
@@ -71,11 +72,17 @@ func (p InteractiveSelectPrinter) WithMaxHeight(maxHeight int) *InteractiveSelec
 	return &p
 }
 
+// OnInterrupt sets the function to execute on exit of the input reader
+func (p InteractiveSelectPrinter) WithOnInterruptFunc(exitFunc func()) *InteractiveSelectPrinter {
+	p.OnInterruptFunc = exitFunc
+	return &p
+}
+
 // Show shows the interactive select menu and returns the selected entry.
 func (p *InteractiveSelectPrinter) Show(text ...string) (string, error) {
 	// should be the first defer statement to make sure it is executed last
 	// and all the needed cleanup can be done before
-	cancel, exit := internal.NewCancelationSignal()
+	cancel, exit := internal.NewCancelationSignal(p.OnInterruptFunc)
 	defer exit()
 
 	if len(text) == 0 || Sprint(text[0]) == "" {
diff --git a/interactive_textinput_printer.go b/interactive_textinput_printer.go
index 512e24f9..111788ae 100644
--- a/interactive_textinput_printer.go
+++ b/interactive_textinput_printer.go
@@ -21,10 +21,11 @@ var (
 
 // InteractiveTextInputPrinter is a printer for interactive select menus.
 type InteractiveTextInputPrinter struct {
-	TextStyle   *Style
-	DefaultText string
-	MultiLine   bool
-	Mask        string
+	TextStyle       *Style
+	DefaultText     string
+	MultiLine       bool
+	Mask            string
+	OnInterruptFunc func()
 
 	input      []string
 	cursorXPos int
@@ -56,11 +57,17 @@ func (p InteractiveTextInputPrinter) WithMask(mask string) *InteractiveTextInput
 	return &p
 }
 
+// OnInterrupt sets the function to execute on exit of the input reader
+func (p InteractiveTextInputPrinter) WithOnInterruptFunc(exitFunc func()) *InteractiveTextInputPrinter {
+	p.OnInterruptFunc = exitFunc
+	return &p
+}
+
 // Show shows the interactive select menu and returns the selected entry.
 func (p InteractiveTextInputPrinter) Show(text ...string) (string, error) {
 	// should be the first defer statement to make sure it is executed last
 	// and all the needed cleanup can be done before
-	cancel, exit := internal.NewCancelationSignal()
+	cancel, exit := internal.NewCancelationSignal(p.OnInterruptFunc)
 	defer exit()
 
 	var areaText string
diff --git a/internal/cancelation_signal.go b/internal/cancelation_signal.go
index d6f79b0a..ed424daf 100644
--- a/internal/cancelation_signal.go
+++ b/internal/cancelation_signal.go
@@ -1,19 +1,16 @@
 package internal
 
-import (
-	"os"
-)
-
 // NewCancelationSignal for keeping track of a cancelation
-func NewCancelationSignal() (func(), func()) {
+func NewCancelationSignal(interruptFunc func()) (func(), func()) {
 	canceled := false
 
 	cancel := func() {
 		canceled = true
 	}
+
 	exit := func() {
-		if canceled {
-			os.Exit(1)
+		if canceled && interruptFunc != nil {
+			interruptFunc()
 		}
 	}
 


Test patch:
diff --git a/interactive_confirm_printer_test.go b/interactive_confirm_printer_test.go
index c6320c78..44644911 100644
--- a/interactive_confirm_printer_test.go
+++ b/interactive_confirm_printer_test.go
@@ -1,6 +1,7 @@
 package pterm_test
 
 import (
+	"reflect"
 	"testing"
 
 	"atomicgo.dev/keyboard"
@@ -126,3 +127,14 @@ func TestInteractiveConfirmPrinter_WithTextStyle(t *testing.T) {
 	p := pterm.DefaultInteractiveConfirm.WithTextStyle(style)
 	testza.AssertEqual(t, p.TextStyle, style)
 }
+
+func TestInteractiveConfirmPrinter_WithOnInterruptFunc(t *testing.T) {
+	// OnInterrupt function defaults to nil
+	pd := pterm.InteractiveConfirmPrinter{}
+	testza.AssertNil(t, pd.OnInterruptFunc)
+
+	// Verify OnInterrupt is set
+	exitfunc := func() {}
+	p := pterm.DefaultInteractiveConfirm.WithOnInterruptFunc(exitfunc)
+	testza.AssertEqual(t, reflect.ValueOf(p.OnInterruptFunc).Pointer(), reflect.ValueOf(exitfunc).Pointer())
+}
diff --git a/interactive_multiselect_printer_test.go b/interactive_multiselect_printer_test.go
index e34d0c8e..a58f568b 100644
--- a/interactive_multiselect_printer_test.go
+++ b/interactive_multiselect_printer_test.go
@@ -1,6 +1,7 @@
 package pterm_test
 
 import (
+	"reflect"
 	"testing"
 
 	"atomicgo.dev/keyboard"
@@ -66,3 +67,14 @@ func TestInteractiveMultiselectPrinter_WithCheckmark(t *testing.T) {
 	p := pterm.DefaultInteractiveMultiselect.WithCheckmark(&pterm.Checkmark{Checked: "+", Unchecked: "-"}).WithOptions([]string{"a", "b", "c"})
 	testza.AssertEqual(t, p.Checkmark, &pterm.Checkmark{Checked: "+", Unchecked: "-"})
 }
+
+func TestInteractiveMultiselectPrinter_WithOnInterruptFunc(t *testing.T) {
+	// OnInterrupt function defaults to nil
+	pd := pterm.InteractiveMultiselectPrinter{}
+	testza.AssertNil(t, pd.OnInterruptFunc)
+
+	// Verify OnInterrupt is set
+	exitfunc := func() {}
+	p := pterm.DefaultInteractiveMultiselect.WithOnInterruptFunc(exitfunc)
+	testza.AssertEqual(t, reflect.ValueOf(p.OnInterruptFunc).Pointer(), reflect.ValueOf(exitfunc).Pointer())
+}
diff --git a/interactive_select_printer_test.go b/interactive_select_printer_test.go
index 5a4c5a90..d4af8b6e 100644
--- a/interactive_select_printer_test.go
+++ b/interactive_select_printer_test.go
@@ -1,6 +1,7 @@
 package pterm_test
 
 import (
+	"reflect"
 	"testing"
 
 	"atomicgo.dev/keyboard"
@@ -49,3 +50,14 @@ func TestInteractiveSelectPrinter_WithMaxHeight(t *testing.T) {
 	p := pterm.DefaultInteractiveSelect.WithMaxHeight(1337)
 	testza.AssertEqual(t, p.MaxHeight, 1337)
 }
+
+func TestInteractiveSelectPrinter_WithOnInterruptFunc(t *testing.T) {
+	// OnInterrupt function defaults to nil
+	pd := pterm.InteractiveSelectPrinter{}
+	testza.AssertNil(t, pd.OnInterruptFunc)
+
+	// Verify OnInterrupt is set
+	exitfunc := func() {}
+	p := pterm.DefaultInteractiveSelect.WithOnInterruptFunc(exitfunc)
+	testza.AssertEqual(t, reflect.ValueOf(p.OnInterruptFunc).Pointer(), reflect.ValueOf(exitfunc).Pointer())
+}
diff --git a/interactive_textinput_printer_test.go b/interactive_textinput_printer_test.go
index a85fe338..e617d655 100644
--- a/interactive_textinput_printer_test.go
+++ b/interactive_textinput_printer_test.go
@@ -1,6 +1,7 @@
 package pterm_test
 
 import (
+	"reflect"
 	"testing"
 
 	"atomicgo.dev/keyboard"
@@ -41,3 +42,14 @@ func TestInteractiveTextInputPrinter_WithMask(t *testing.T) {
 	result, _ := pterm.DefaultInteractiveTextInput.WithMask("*").Show()
 	testza.AssertEqual(t, result, "abc")
 }
+
+func TestInteractiveTextInputPrinter_WithOnInterruptFunc(t *testing.T) {
+	// OnInterrupt function defaults to nil
+	pd := pterm.InteractiveTextInputPrinter{}
+	testza.AssertNil(t, pd.OnInterruptFunc)
+
+	// Verify OnInterrupt is set
+	exitfunc := func() {}
+	p := pterm.DefaultInteractiveTextInput.WithOnInterruptFunc(exitfunc)
+	testza.AssertEqual(t, reflect.ValueOf(p.OnInterruptFunc).Pointer(), reflect.ValueOf(exitfunc).Pointer())
+}