Repository: pterm/pterm
Pull request #593 (2023-12-16 14:47:47)

fix: exit on `ctrl+c` in interactive printers by default
### Description

Adding a default behavior to cancelation signals when they are nil.  
When a function is not provided it uses `os.Exit(1)` which is expected as default behavior.
Using an `internal.Exit` so we can mock `os.Exit` functionality for testing
Updated a test that was terminated by `os.Exit` behavior

### Scope
> What is affected by this pull request?

- [X] Bug Fix
- [ ] New Feature
- [ ] Documentation
- [ ] Other

### Related Issue

Fixes https://github.com/pterm/pterm/issues/592

### To-Do Checklist
- [x] I tested my changes
- [ ] 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/593?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pterm) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Comparison is base [(`4d9914e`)](https://app.codecov.io/gh/pterm/pterm/commit/4d9914e569553af7e10e552c9a7a98f442f4b93f?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pterm) 81.83% compared to head [(`b0bcda1`)](https://app.codecov.io/gh/pterm/pterm/pull/593?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pterm) 81.83%.
> Report is 1 commits behind head on master.


<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##           master     #593   +/-   ##
=======================================
  Coverage   81.83%   81.83%           
=======================================
  Files          33       33           
  Lines        4118     4118           
=======================================
  Hits         3370     3370           
  Misses        684      684           
  Partials       64       64           
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/pterm/pterm/pull/593?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pterm).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pterm).


Patch:
diff --git a/interactive_continue_printer.go b/interactive_continue_printer.go
index f7e49e8a..4049cde9 100644
--- a/interactive_continue_printer.go
+++ b/interactive_continue_printer.go
@@ -2,7 +2,6 @@ package pterm
 
 import (
 	"fmt"
-	"os"
 	"strings"
 
 	"atomicgo.dev/cursor"
@@ -155,7 +154,7 @@ func (p InteractiveContinuePrinter) Show(text ...string) (string, error) {
 			result = p.Options[p.DefaultValueIndex]
 			return true, nil
 		case keys.CtrlC:
-			os.Exit(1)
+			internal.Exit(1)
 			return true, nil
 		}
 		return false, nil
diff --git a/internal/cancelation_signal.go b/internal/cancelation_signal.go
index ed424daf..33f17cc1 100644
--- a/internal/cancelation_signal.go
+++ b/internal/cancelation_signal.go
@@ -9,8 +9,12 @@ func NewCancelationSignal(interruptFunc func()) (func(), func()) {
 	}
 
 	exit := func() {
-		if canceled && interruptFunc != nil {
-			interruptFunc()
+		if canceled {
+			if interruptFunc != nil {
+				interruptFunc()
+			} else {
+				Exit(1)
+			}
 		}
 	}
 
diff --git a/internal/exit.go b/internal/exit.go
new file mode 100644
index 00000000..f29f74e8
--- /dev/null
+++ b/internal/exit.go
@@ -0,0 +1,14 @@
+package internal
+
+import "os"
+
+// ExitFuncType is the type of function used to exit the program.
+type ExitFuncType func(int)
+
+// DefaultExitFunc is the default function used to exit the program.
+var DefaultExitFunc ExitFuncType = os.Exit
+
+// Exit calls the current exit function.
+func Exit(code int) {
+    DefaultExitFunc(code)
+}
\ No newline at end of file


Test patch:
diff --git a/interactive_textinput_printer_test.go b/interactive_textinput_printer_test.go
index 2f925a02..8d9c54c8 100644
--- a/interactive_textinput_printer_test.go
+++ b/interactive_textinput_printer_test.go
@@ -1,6 +1,7 @@
 package pterm_test
 
 import (
+	"os"
 	"reflect"
 	"testing"
 	"time"
@@ -10,6 +11,7 @@ import (
 	"github.com/MarvinJWendt/testza"
 
 	"github.com/pterm/pterm"
+	"github.com/pterm/pterm/internal"
 )
 
 func TestInteractiveTextInputPrinter_WithDefaultText(t *testing.T) {
@@ -56,12 +58,23 @@ func TestInteractiveTextInputPrinter_WithMask(t *testing.T) {
 }
 
 func TestInteractiveTextInputPrinter_WithCancel(t *testing.T) {
-	go func() {
-		time.Sleep(1 * time.Millisecond)
-		keyboard.SimulateKeyPress(keys.CtrlC)
-	}()
-	result, _ := pterm.DefaultInteractiveTextInput.WithMask("*").Show()
-	testza.AssertEqual(t, "", result)
+    exitCalled := false
+    internal.DefaultExitFunc = func(code int) {
+        exitCalled = true
+    }
+    defer func() { internal.DefaultExitFunc = os.Exit }()
+
+    go func() {
+        time.Sleep(1 * time.Millisecond)
+        keyboard.SimulateKeyPress(keys.CtrlC)
+    }()
+
+    result, _ := pterm.DefaultInteractiveTextInput.WithMask("*").Show()
+    testza.AssertEqual(t, "", result)
+
+    if !exitCalled {
+        t.Errorf("Expected exit to be called on Ctrl+C")
+    }
 }
 
 func TestInteractiveTextInputPrinter_OnEnter(t *testing.T) {
diff --git a/internal/cancelation_signal_test.go b/internal/cancelation_signal_test.go
new file mode 100644
index 00000000..5972bc66
--- /dev/null
+++ b/internal/cancelation_signal_test.go
@@ -0,0 +1,54 @@
+package internal
+
+import (
+	"os"
+	"testing"
+)
+
+func TestNewCancelationSignal(t *testing.T) {
+    // Mock DefaultExitFunc
+    exitCalled := false
+    exitCode := 0
+    DefaultExitFunc = func(code int) {
+        exitCalled = true
+        exitCode = code
+    }
+    defer func() { DefaultExitFunc = os.Exit }() // Reset after tests
+
+    // Scenario 1: Testing cancel function
+    cancel, exit := NewCancelationSignal(nil)
+    if exitCalled {
+        t.Error("Exit function should not be called immediately after NewCancelationSignal")
+    }
+
+    // Scenario 2: Testing exit function without cancel
+    exit()
+    if exitCalled {
+        t.Error("Exit function should not be called when cancel is not set")
+    }
+
+    // Scenario 3: Testing cancel then exit with no interruptFunc
+    cancel()
+    exit()
+    if !exitCalled || exitCode != 1 {
+        t.Errorf("Expected Exit(1) to be called, exitCalled: %v, exitCode: %d", exitCalled, exitCode)
+    }
+
+    // Reset for next scenario
+    exitCalled = false
+    exitCode = 0
+
+    // Scenario 4: Testing cancel then exit with interruptFunc
+    interruptCalled := false
+    cancel, exit = NewCancelationSignal(func() {
+        interruptCalled = true
+    })
+    cancel()
+    exit()
+    if interruptCalled == false {
+        t.Error("Expected interruptFunc to be called")
+    }
+    if exitCalled {
+        t.Error("Exit should not be called when interruptFunc is provided")
+    }
+}
diff --git a/internal/exit_test.go b/internal/exit_test.go
new file mode 100644
index 00000000..5b15473b
--- /dev/null
+++ b/internal/exit_test.go
@@ -0,0 +1,23 @@
+package internal_test
+
+import (
+	"os"
+	"testing"
+
+	"github.com/pterm/pterm/internal"
+)
+
+func TestExit(t *testing.T) {
+    var lastExitCode int
+    internal.DefaultExitFunc = func(code int) {
+        lastExitCode = code
+    }
+
+    defer func() { internal.DefaultExitFunc = os.Exit }() 
+
+    internal.Exit(1)
+
+    if lastExitCode != 1 {
+        t.Errorf("Expected exit code 1, got %d", lastExitCode)
+    }
+}