Repository: pterm/pterm
Pull request #384 (2022-07-26 14:28:36)

feat: adding interactive continue printer
### Description

Adding a new interactive printer, it's very similar to the confirmation printer but allows for multiple options.

```console
$ go run main.go                                           
Do you want to continue [Y/n/a/s]: 

 INFO  You answered: no
```

### Scope
> What is affected by this pull request?

- [ ] Bug Fix
- [x] New Feature
- [ ] Documentation
- [ ] Other

### To-Do Checklist
- [x] I tested my changes
- [x] I have commented every method that I created/changed
- [x] I updated the examples to fit with my changes
- [x] I have added tests for my newly created methods


codecov[bot]: # [Codecov](https://codecov.io/gh/pterm/pterm/pull/384?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pterm) Report
> Merging [#384](https://codecov.io/gh/pterm/pterm/pull/384?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pterm) (2b8cd95) into [master](https://codecov.io/gh/pterm/pterm/commit/e373573de744abcbf394b196b9d5a7648eb51c9f?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pterm) (e373573) will **decrease** coverage by `0.03%`.
> The diff coverage is `80.35%`.

```diff
@@            Coverage Diff             @@
##           master     #384      +/-   ##
==========================================
- Coverage   81.78%   81.74%   -0.04%     
==========================================
  Files          28       29       +1     
  Lines        2015     2071      +56     
==========================================
+ Hits         1648     1693      +45     
- Misses        355      362       +7     
- Partials       12       16       +4     
```


| [Impacted Files](https://codecov.io/gh/pterm/pterm/pull/384?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pterm) | Coverage Δ | |
|---|---|---|
| [interactive\_continue\_printer.go](https://codecov.io/gh/pterm/pterm/pull/384/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pterm#diff-aW50ZXJhY3RpdmVfY29udGludWVfcHJpbnRlci5nbw==) | `80.35% <80.35%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/pterm/pterm/pull/384?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pterm).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pterm)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/pterm/pterm/pull/384?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pterm). Last update [e373573...2b8cd95](https://codecov.io/gh/pterm/pterm/pull/384?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pterm). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pterm).

MarvinJWendt: Hi, sorry, we're lacking a bit of a contributing file. You don't have to edit any markdown or SVG files. If you add an example, you only need to commit the `_examples/xxx/demo/main.go` file. Everything else will be generated automatically, when the PR is merged. You don't have to revert the files tho, they'll get overwritten by the CI.

I'll review the code later, thanks for the PR!
luisdavim: > Hi, sorry, we're lacking a bit of a contributing file. You don't have to edit any markdown or SVG files. If you add an example, you only need to commit the `_examples/xxx/demo/main.go` file. Everything else will be generated automatically when the PR is merged. You don't have to revert the files tho, they'll get overwritten by the CI.
> 
> I'll review the code later, thanks for the PR!

oh, ok, do you want me to revert those changes?
MarvinJWendt: Hi @luisdavim, sorry for the delay, I am on vacation since the first August. I didn't have time to look into it before. 
luisdavim: No worries, enjoy your time off, this will be here waiting when you get back 😉 
MarvinJWendt: Hi @luisdavim, what do you think about adding this as a feature to the confirm printer? I don't really see the benefit in creating a new printer for it, as they would be pretty much the same.
luisdavim: Sure, I can think about how that would look like. 
luisdavim: Hi @MarvinJWendt, sorry for the delay on this. I was thinking about merging this and the confirm printers but I don't think there's a good way, we could replace the confirm printer with this one but that would be a breaking change, the confirm printer returns a `bool` and that only works if there are only 2 options. I do think there's room for both, the confirm printer covers the simpler use cases where only 2 options are needed and it is better in that situation because it returns a `bool` but there are many cases where something like `yes/no/always/never` are needed and that's where this printer comes in handy, before pterm had the interactive printers I was using https://github.com/AlecAivazis/survey and to cover this situation I had to use the select prompt with a custom filter, it works but it's not as cleann, something like this:

```go
func prompt(filename string) (string, error) {
	var response string
	if _, err := os.Stat(filename); err == nil {
		prompt := &survey.Select{
			Message: fmt.Sprintf("File %s exists. Overwrite?", filename),
			Options: []string{
				"diff: show a diff between the current file and the new file",
				"yes: overwrite the current file",
				"no: do not overwrite the current file",
				"all: overwrite all files",
				"None: do not overwrite any files",
				"quit: exit the program",
			},
			Filter: func(filterValue string, optValue string, optIndex int) bool {
				return strings.HasPrefix(optValue, filterValue)
			},
		}
		err = survey.AskOne(prompt, &response)
		return strings.ToLower(strings.Split(response, ":")[0]), err
	}
	return response, nil
}
```
MarvinJWendt: No problem 😉 
Yes, that makes sense to me. 

Patch:
diff --git a/README.md b/README.md
index 7433dc04..eb0acfee 100644
--- a/README.md
+++ b/README.md
@@ -90,7 +90,7 @@ which features automatic website generation, automatic deployments, a custom CI-
 |✏ Documentation |To view the official documentation of the latest release, you can go to the automatically generated page of [pkg.go.dev](https://pkg.go.dev/github.com/pterm/pterm#section-documentation) This documentation is very technical and includes every method that can be used in PTerm.<br>**For an easy start we recommend that you take a look at the [examples section](#-examples).** Here you can see pretty much every feature of PTerm with example code. The animations of the examples are automatically updated as soon as something changes in PTerm.|
 
 <div align="center">
-	
+
 ### Printers (Components)
 
 |Feature|Examples| - |Feature|Examples|
@@ -113,7 +113,7 @@ which features automatic website generation, automatic deployments, a custom CI-
 |---|---|---|
 |![Jens Lauterbach](https://avatars.githubusercontent.com/u/1292368?s=25)|[@jenslauterbach](https://github.com/jenslauterbach)|25$|
 
-</div>	
+</div>
 
 ## 🧪 Examples
 
@@ -1066,6 +1066,31 @@ func boolToText(b bool) string {
 
 </details>
 
+### interactive_continue/demo
+
+![Animation](https://raw.githubusercontent.com/pterm/pterm/master/_examples/interactive_continue/demo/animation.svg)
+
+<details>
+
+<summary>SHOW SOURCE</summary>
+
+```go
+package main
+
+import (
+	"github.com/pterm/pterm"
+)
+
+func main() {
+	result, _ := pterm.DefaultInteractiveContinue.Show()
+	pterm.Println() // Blank line
+	pterm.Info.Printfln("You answered: %s", result)
+}
+
+```
+
+</details>
+
 ### interactive_multiselect/demo
 
 ![Animation](https://raw.githubusercontent.com/pterm/pterm/master/_examples/interactive_multiselect/demo/animation.svg)
@@ -1608,7 +1633,7 @@ func main() {
 
 <!-- examples:end -->
 
-  
+
 ---
 
 > GitHub [@pterm](https://github.com/pterm) &nbsp;&middot;&nbsp;
diff --git a/_examples/README.md b/_examples/README.md
index 12dad628..41cc9848 100644
--- a/_examples/README.md
+++ b/_examples/README.md
@@ -938,6 +938,31 @@ func boolToText(b bool) string {
 
 </details>
 
+### interactive_continue/demo
+
+![Animation](https://raw.githubusercontent.com/pterm/pterm/master/_examples/interactive_continue/demo/animation.svg)
+
+<details>
+
+<summary>SHOW SOURCE</summary>
+
+```go
+package main
+
+import (
+	"github.com/pterm/pterm"
+)
+
+func main() {
+	result, _ := pterm.DefaultInteractiveContinue.Show()
+	pterm.Println() // Blank line
+	pterm.Info.Printfln("You answered: %s", result)
+}
+
+```
+
+</details>
+
 ### interactive_multiselect/demo
 
 ![Animation](https://raw.githubusercontent.com/pterm/pterm/master/_examples/interactive_multiselect/demo/animation.svg)
diff --git a/_examples/interactive_continue/README.md b/_examples/interactive_continue/README.md
new file mode 100644
index 00000000..3aa4b33a
--- /dev/null
+++ b/_examples/interactive_continue/README.md
@@ -0,0 +1,25 @@
+### interactive_continue/demo
+
+![Animation](https://raw.githubusercontent.com/pterm/pterm/master/_examples/interactive_continue/demo/animation.svg)
+
+<details>
+
+<summary>SHOW SOURCE</summary>
+
+```go
+package main
+
+import (
+	"github.com/pterm/pterm"
+)
+
+func main() {
+	result, _ := pterm.DefaultInteractiveContinue.Show()
+	pterm.Println() // Blank line
+	pterm.Info.Printfln("You answered: %s", result)
+}
+
+```
+
+</details>
+
diff --git a/_examples/interactive_continue/demo/README.md b/_examples/interactive_continue/demo/README.md
new file mode 100644
index 00000000..f4c20783
--- /dev/null
+++ b/_examples/interactive_continue/demo/README.md
@@ -0,0 +1,18 @@
+# interactive_continue/demo
+
+![Animation](animation.svg)
+
+```go
+package main
+
+import (
+	"github.com/pterm/pterm"
+)
+
+func main() {
+	result, _ := pterm.DefaultInteractiveContinue.Show()
+	pterm.Println() // Blank line
+	pterm.Info.Printfln("You answered: %s", result)
+}
+
+```
diff --git a/_examples/interactive_continue/demo/animation.svg b/_examples/interactive_continue/demo/animation.svg
new file mode 100644
index 00000000..5ec0f59c
--- /dev/null
+++ b/_examples/interactive_continue/demo/animation.svg
@@ -0,0 +1,10 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="840" height="581.04"><rect width="840" height="581.04" rx="5" ry="5" class="a"></rect><svg y="0%" x="0%"><circle cx="20" cy="20" r="6" class="b"></circle><circle cx="40" cy="20" r="6" class="c"></circle><circle cx="60" cy="20" r="6" class="d"></circle></svg><svg height="521.04" viewBox="0 0 269 260.52" width="800" x="15" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" y="50"><style>.e, .g, .f {
+	font-family:
+			'Courier New'
+			Roboto,
+			Helvetica,
+			Arial,
+			sans-serif,
+			'Apple Color Emoji',
+			'Segoe UI Emoji' !important;
+}.a{fill:rgb(35,35,35)}.b{fill:#ff5f58}.c{fill:#ffbd2e}.d{fill:#18c132}.e{font-family:'Courier New',Monaco,Consolas,Menlo,'Bitstream Vera Sans Mono','Powerline Symbols',monospace}.f{fill:rgb(126,231,233);white-space:pre}.g{fill:rgb(234,89,246);white-space:pre}.h{fill:rgb(102,194,205)}.i{white-space:pre}.j{fill:transparent}.k{fill:rgb(160,160,160)}.m{animation-duration:6.183908s;animation-iteration-count:infinite;animation-name:l;animation-timing-function:steps(1,end)}.n{fill:rgb(160,160,160);white-space:pre}@keyframes l{0%{transform:translateX(-0px)}2.5%{transform:translateX(-269px)}19.1%{transform:translateX(-538px)}19.1%{transform:translateX(-807px)}100%{transform:translateX(-1076px)}}</style><g font-family="'Courier New',Monaco, Consolas, Menlo, &#x27;Bitstream Vera Sans Mono&#x27;, &#x27;Powerline Symbols&#x27;, monospace" font-size="1.67" class="e"><defs><symbol id="1"><text x="0" y="1.67" class="f">Do</text><text x="3.006" y="1.67" class="f">you</text><text x="7.013999999999999" y="1.67" class="f">want</text><text x="12.024" y="1.67" class="f">to</text><text x="15.029999999999998" y="1.67" class="f">continue</text><text x="24.048" y="1.67" class="g">[Y/n/a/s]</text><text x="33.065999999999995" y="1.67" class="f">:</text></symbol><symbol id="2"><rect height="2.171" width="6" x="0" y="0" class="h"></rect><text x="0" y="1.67" class="i"> INFO </text><text x="7.013999999999999" y="1.67" class="f">You</text><text x="11.021999999999998" y="1.67" class="f">answered:</text><text x="21.041999999999998" y="1.67" class="f">yes</text></symbol><symbol id="a"><rect height="120" width="269" x="0" y="0" class="j"></rect></symbol><symbol id="b"><rect height="2.171" width="1.1022" class="k"></rect></symbol></defs><rect height="260.52" width="269" class="a"></rect><g class="m"><svg x="0" y="0" width="1345"><svg x="0"><use xlink:href="#a"></use><use xlink:href="#b" x="-0.0040000000000000036" y="0"></use></svg><svg x="269"><use xlink:href="#a"></use><use xlink:href="#b" x="34.996" y="0"></use><use xlink:href="#1" y="0"></use></svg><svg x="538"><use xlink:href="#a"></use><use xlink:href="#b" x="-0.0040000000000000036" y="2.146"></use><use xlink:href="#1" y="0"></use></svg><svg x="807"><use xlink:href="#a"></use><use xlink:href="#b" x="-0.0040000000000000036" y="6.4879999999999995"></use><use xlink:href="#1" y="0"></use><use xlink:href="#2" y="4.342"></use></svg><svg x="1076"><use xlink:href="#a"></use><use xlink:href="#b" x="22.996" y="10.83"></use><use xlink:href="#1" y="0"></use><use xlink:href="#2" y="4.342"></use><text x="0" y="10.354" class="n">Restarting</text><text x="11.021999999999998" y="10.354" class="n">animation...</text></svg></svg></g></g></svg></svg>
diff --git a/_examples/interactive_continue/demo/ci.go b/_examples/interactive_continue/demo/ci.go
new file mode 100644
index 00000000..2ba46081
--- /dev/null
+++ b/_examples/interactive_continue/demo/ci.go
@@ -0,0 +1,19 @@
+package main
+
+import (
+	"os"
+	"time"
+
+	"atomicgo.dev/keyboard"
+)
+
+// ------ Automation for CI ------
+// You can ignore this function, it is used to automatically run the demo and generate the example animation in our CI system.
+func init() {
+	if os.Getenv("CI") == "true" {
+		go func() {
+			time.Sleep(time.Second * 2)
+			keyboard.SimulateKeyPress('y')
+		}()
+	}
+}
diff --git a/_examples/interactive_continue/demo/main.go b/_examples/interactive_continue/demo/main.go
new file mode 100644
index 00000000..6684bdf9
--- /dev/null
+++ b/_examples/interactive_continue/demo/main.go
@@ -0,0 +1,11 @@
+package main
+
+import (
+	"github.com/pterm/pterm"
+)
+
+func main() {
+	result, _ := pterm.DefaultInteractiveContinue.Show()
+	pterm.Println() // Blank line
+	pterm.Info.Printfln("You answered: %s", result)
+}
diff --git a/docs/README.md b/docs/README.md
index 7433dc04..eb0acfee 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -90,7 +90,7 @@ which features automatic website generation, automatic deployments, a custom CI-
 |✏ Documentation |To view the official documentation of the latest release, you can go to the automatically generated page of [pkg.go.dev](https://pkg.go.dev/github.com/pterm/pterm#section-documentation) This documentation is very technical and includes every method that can be used in PTerm.<br>**For an easy start we recommend that you take a look at the [examples section](#-examples).** Here you can see pretty much every feature of PTerm with example code. The animations of the examples are automatically updated as soon as something changes in PTerm.|
 
 <div align="center">
-	
+
 ### Printers (Components)
 
 |Feature|Examples| - |Feature|Examples|
@@ -113,7 +113,7 @@ which features automatic website generation, automatic deployments, a custom CI-
 |---|---|---|
 |![Jens Lauterbach](https://avatars.githubusercontent.com/u/1292368?s=25)|[@jenslauterbach](https://github.com/jenslauterbach)|25$|
 
-</div>	
+</div>
 
 ## 🧪 Examples
 
@@ -1066,6 +1066,31 @@ func boolToText(b bool) string {
 
 </details>
 
+### interactive_continue/demo
+
+![Animation](https://raw.githubusercontent.com/pterm/pterm/master/_examples/interactive_continue/demo/animation.svg)
+
+<details>
+
+<summary>SHOW SOURCE</summary>
+
+```go
+package main
+
+import (
+	"github.com/pterm/pterm"
+)
+
+func main() {
+	result, _ := pterm.DefaultInteractiveContinue.Show()
+	pterm.Println() // Blank line
+	pterm.Info.Printfln("You answered: %s", result)
+}
+
+```
+
+</details>
+
 ### interactive_multiselect/demo
 
 ![Animation](https://raw.githubusercontent.com/pterm/pterm/master/_examples/interactive_multiselect/demo/animation.svg)
@@ -1608,7 +1633,7 @@ func main() {
 
 <!-- examples:end -->
 
-  
+
 ---
 
 > GitHub [@pterm](https://github.com/pterm) &nbsp;&middot;&nbsp;
diff --git a/interactive_confirm_printer.go b/interactive_confirm_printer.go
index 31dbafd9..dc37e597 100644
--- a/interactive_confirm_printer.go
+++ b/interactive_confirm_printer.go
@@ -90,7 +90,7 @@ func (p InteractiveConfirmPrinter) WithSuffixStyle(style *Style) *InteractiveCon
 //
 // Example:
 //  result, _ := pterm.DefaultInteractiveConfirm.Show("Are you sure?")
-//	pterm.Println(result)
+//  pterm.Println(result)
 func (p InteractiveConfirmPrinter) Show(text ...string) (bool, error) {
 	var result bool
 
diff --git a/interactive_continue_printer.go b/interactive_continue_printer.go
new file mode 100644
index 00000000..3ecb03c7
--- /dev/null
+++ b/interactive_continue_printer.go
@@ -0,0 +1,189 @@
+package pterm
+
+import (
+	"fmt"
+	"os"
+	"strings"
+
+	"atomicgo.dev/cursor"
+	"atomicgo.dev/keyboard"
+	"atomicgo.dev/keyboard/keys"
+	"golang.org/x/text/cases"
+	"golang.org/x/text/language"
+)
+
+var (
+	// DefaultInteractiveContinue is the default InteractiveContinue printer.
+	// Pressing "y" will return yes, "n" will return no, "a" returns all and "s" returns stop.
+	// Pressing enter without typing any letter will return the configured default value (by default set to "yes", the fisrt option).
+	DefaultInteractiveContinue = InteractiveContinuePrinter{
+		DefaultValueIndex: 0,
+		DefaultText:       "Do you want to continue",
+		TextStyle:         &ThemeDefault.PrimaryStyle,
+		Options:           []string{"yes", "no", "all", "cancel"},
+		OptionsStyle:      &ThemeDefault.SuccessMessageStyle,
+		SuffixStyle:       &ThemeDefault.SecondaryStyle,
+	}
+)
+
+// InteractiveContinuePrinter is a printer for interactive continue prompts.
+type InteractiveContinuePrinter struct {
+	DefaultValueIndex int
+	DefaultText       string
+	TextStyle         *Style
+	Options           []string
+	OptionsStyle      *Style
+	Handles           []string
+	ShowShortHandles  bool
+	SuffixStyle       *Style
+}
+
+// WithDefaultText sets the default text.
+func (p InteractiveContinuePrinter) WithDefaultText(text string) *InteractiveContinuePrinter {
+	p.DefaultText = text
+	return &p
+}
+
+// WithDefaultValueIndex sets the default value, which will be returned when the user presses enter without typing any letter.
+func (p InteractiveContinuePrinter) WithDefaultValueIndex(value int) *InteractiveContinuePrinter {
+	if value >= len(p.Options) {
+		panic("Index out of range")
+	}
+	p.DefaultValueIndex = value
+	return &p
+}
+
+// WithDefaultValue sets the default value, which will be returned when the user presses enter without typing any letter.
+func (p InteractiveContinuePrinter) WithDefaultValue(value string) *InteractiveContinuePrinter {
+	for i, o := range p.Options {
+		if o == value {
+			p.DefaultValueIndex = i
+			break
+		}
+	}
+	return &p
+}
+
+// WithTextStyle sets the text style.
+func (p InteractiveContinuePrinter) WithTextStyle(style *Style) *InteractiveContinuePrinter {
+	p.TextStyle = style
+	return &p
+}
+
+// WithOptions sets the options.
+func (p InteractiveContinuePrinter) WithOptions(options []string) *InteractiveContinuePrinter {
+	p.Options = options
+	return &p
+}
+
+// WithHandles allows you to customize the short handles for the answers.
+func (p InteractiveContinuePrinter) WithHandles(handles []string) *InteractiveContinuePrinter {
+	if len(handles) != len(p.Options) {
+		Warning.Printf("%v is not a valid set of handles", handles)
+		p.setDefaultHandles()
+		return &p
+	}
+	p.Handles = handles
+	return &p
+}
+
+// WithShowShortHandles will set ShowShortHandles to true
+// this makes the printer display the shorthand options instead their shorthand version.
+func (p InteractiveContinuePrinter) WithShowShortHandles() *InteractiveContinuePrinter {
+	p.ShowShortHandles = true
+	return &p
+}
+
+// WithOptionsStyle sets the continue style.
+func (p InteractiveContinuePrinter) WithOptionsStyle(style *Style) *InteractiveContinuePrinter {
+	p.OptionsStyle = style
+	return &p
+}
+
+// WithSuffixStyle sets the suffix style.
+func (p InteractiveContinuePrinter) WithSuffixStyle(style *Style) *InteractiveContinuePrinter {
+	p.SuffixStyle = style
+	return &p
+}
+
+// Show shows the continue prompt.
+//
+// Example:
+//  result, _ := pterm.DefaultInteractiveContinue.Show("Do you want to apply the changes?")
+//  pterm.Println(result)
+func (p InteractiveContinuePrinter) Show(text ...string) (string, error) {
+	var result string
+
+	if len(text) == 0 || text[0] == "" {
+		text = []string{p.DefaultText}
+	}
+
+	p.TextStyle.Print(text[0] + " " + p.getSuffix() + ": ")
+
+	err := keyboard.Listen(func(keyInfo keys.Key) (stop bool, err error) {
+		if err != nil {
+			return false, fmt.Errorf("failed to get key: %w", err)
+		}
+		key := keyInfo.Code
+		char := keyInfo.String()
+
+		switch key {
+		case keys.RuneKey:
+			for i, c := range p.Handles {
+				if !p.ShowShortHandles {
+					c = string([]rune(c)[0])
+				}
+				if char == c || (i == p.DefaultValueIndex && strings.EqualFold(c, char)) {
+					p.OptionsStyle.Print(p.Options[i])
+					Println()
+					result = p.Options[i]
+					return true, nil
+				}
+			}
+		case keys.Enter:
+			p.OptionsStyle.Print(p.Options[p.DefaultValueIndex])
+			Println()
+			result = p.Options[p.DefaultValueIndex]
+			return true, nil
+		case keys.CtrlC:
+			os.Exit(1)
+			return true, nil
+		}
+		return false, nil
+	})
+	cursor.StartOfLine()
+	return result, err
+}
+
+// getShortHandles returns the short hand answers for the continueation prompt
+func (p InteractiveContinuePrinter) getShortHandles() []string {
+	var handles []string
+	for _, option := range p.Options {
+		handles = append(handles, strings.ToLower(string([]rune(option)[0])))
+	}
+	handles[p.DefaultValueIndex] = strings.ToUpper(handles[p.DefaultValueIndex])
+
+	return handles
+}
+
+// setDefaultHandles initialises the handles
+func (p *InteractiveContinuePrinter) setDefaultHandles() {
+	if p.ShowShortHandles {
+		p.Handles = p.getShortHandles()
+	}
+
+	if p.Handles == nil || len(p.Handles) == 0 {
+		p.Handles = make([]string, len(p.Options))
+		copy(p.Handles, p.Options)
+		p.Handles[p.DefaultValueIndex] = cases.Title(language.Und, cases.Compact).String(p.Handles[p.DefaultValueIndex])
+	}
+}
+
+// getSuffix returns the continuation prompt suffix
+func (p *InteractiveContinuePrinter) getSuffix() string {
+	if p.Handles == nil || len(p.Handles) != len(p.Options) {
+		p.setDefaultHandles()
+	}
+
+	return p.SuffixStyle.Sprintf("[%s]", strings.Join(p.Handles, "/"))
+}


Test patch:
diff --git a/interactive_continue_printer_test.go b/interactive_continue_printer_test.go
new file mode 100644
index 00000000..8686b284
--- /dev/null
+++ b/interactive_continue_printer_test.go
@@ -0,0 +1,183 @@
+package pterm_test
+
+import (
+	"testing"
+
+	"atomicgo.dev/keyboard"
+	"atomicgo.dev/keyboard/keys"
+	"github.com/MarvinJWendt/testza"
+
+	"github.com/pterm/pterm"
+)
+
+func TestInteractiveContinuePrinter_Show_yes(t *testing.T) {
+	go func() {
+		keyboard.SimulateKeyPress('y')
+	}()
+	result, _ := pterm.DefaultInteractiveContinue.Show()
+	testza.AssertEqual(t, result, "yes")
+	go func() {
+		keyboard.SimulateKeyPress('Y')
+	}()
+	result, _ = pterm.DefaultInteractiveContinue.Show()
+	testza.AssertEqual(t, result, "yes")
+}
+
+func TestInteractiveContinuePrinter_Show_no(t *testing.T) {
+	go func() {
+		keyboard.SimulateKeyPress('n')
+	}()
+	result, _ := pterm.DefaultInteractiveContinue.Show()
+	testza.AssertEqual(t, result, "no")
+}
+
+func TestInteractiveContinuePrinter_WithDefaultValueIndes(t *testing.T) {
+	p := pterm.DefaultInteractiveContinue.WithDefaultValueIndex(1)
+	testza.AssertEqual(t, p.DefaultValueIndex, 1)
+}
+
+func TestInteractiveContinuePrinter_WithDefaultValue_yes(t *testing.T) {
+	go func() {
+		keyboard.SimulateKeyPress(keys.Enter)
+	}()
+	p := pterm.DefaultInteractiveContinue.WithDefaultValue("yes")
+	result, _ := p.Show()
+	testza.AssertEqual(t, result, "yes")
+}
+
+func TestInteractiveContinuePrinter_WithDefaultValue_no(t *testing.T) {
+	p := pterm.DefaultInteractiveContinue.WithDefaultValue("no")
+	go func() {
+		keyboard.SimulateKeyPress(keys.Enter)
+	}()
+	result, _ := p.Show()
+	testza.AssertEqual(t, result, "no")
+	go func() {
+		keyboard.SimulateKeyPress('n')
+	}()
+	result, _ = p.Show()
+	testza.AssertEqual(t, result, "no")
+	go func() {
+		keyboard.SimulateKeyPress('N')
+	}()
+	result, _ = p.Show()
+	testza.AssertEqual(t, result, "no")
+}
+
+func TestInteractiveContinuePrinter_WithShowShortHandles(t *testing.T) {
+	p := pterm.DefaultInteractiveContinue.WithShowShortHandles()
+	testza.AssertTrue(t, p.ShowShortHandles)
+	go func() {
+		keyboard.SimulateKeyPress('n')
+	}()
+	result, _ := p.Show()
+	testza.AssertEqual(t, result, "no")
+}
+
+func TestInteractiveContinuePrinter_WithOptionsStyle(t *testing.T) {
+	style := pterm.NewStyle(pterm.FgRed)
+	p := pterm.DefaultInteractiveContinue.WithOptionsStyle(style)
+	testza.AssertEqual(t, p.OptionsStyle, style)
+}
+
+func TestInteractiveContinuePrinter_WithOptions(t *testing.T) {
+	p := pterm.DefaultInteractiveContinue.WithOptions([]string{"next", "stop", "continue"})
+	testza.AssertEqual(t, p.Options, []string{"next", "stop", "continue"})
+}
+
+func TestInteractiveContinuePrinter_WithHandles(t *testing.T) {
+	p := pterm.DefaultInteractiveContinue.WithOptions([]string{"yes", "no", "always", "never"}).WithHandles([]string{"y", "n", "a", "N"})
+	testza.AssertEqual(t, p.Handles, []string{"y", "n", "a", "N"})
+	tests := []struct {
+		name     string
+		key      rune
+		expected string
+	}{
+		{
+			name:     "Yes",
+			key:      'y',
+			expected: "yes",
+		},
+		{
+			name:     "No",
+			key:      'n',
+			expected: "no",
+		},
+		{
+			name:     "Always",
+			key:      'a',
+			expected: "always",
+		},
+		{
+			name:     "Never",
+			key:      'N',
+			expected: "never",
+		},
+	}
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			go func() {
+				keyboard.SimulateKeyPress(tc.key)
+			}()
+			result, _ := p.Show()
+			testza.AssertEqual(t, result, tc.expected)
+		})
+	}
+	p.DefaultValueIndex = 1
+	go func() {
+		keyboard.SimulateKeyPress(keys.Enter)
+	}()
+	result, _ := p.Show()
+	testza.AssertEqual(t, result, "no")
+}
+
+func TestInteractiveContinuePrinter_WithDefaultText(t *testing.T) {
+	p := pterm.DefaultInteractiveContinue.WithDefaultText("default")
+	testza.AssertEqual(t, p.DefaultText, "default")
+}
+
+func TestInteractiveContinuePrinter_CustomAnswers(t *testing.T) {
+	p := pterm.DefaultInteractiveContinue.WithOptions([]string{"next", "stop", "continue"})
+	tests := []struct {
+		name     string
+		key      rune
+		expected string
+	}{
+		{
+			name:     "Next",
+			key:      'n',
+			expected: "next",
+		},
+		{
+			name:     "Stop",
+			key:      's',
+			expected: "stop",
+		},
+		{
+			name:     "Continue",
+			key:      'c',
+			expected: "continue",
+		},
+	}
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			go func() {
+				keyboard.SimulateKeyPress(tc.key)
+			}()
+			result, _ := p.Show()
+			testza.AssertEqual(t, result, tc.expected)
+		})
+	}
+}
+
+func TestInteractiveContinuePrinter_WithSuffixStyle(t *testing.T) {
+	style := pterm.NewStyle(pterm.FgRed)
+	p := pterm.DefaultInteractiveContinue.WithSuffixStyle(style)
+	testza.AssertEqual(t, p.SuffixStyle, style)
+}
+
+func TestInteractiveContinuePrinter_WithTextStyle(t *testing.T) {
+	style := pterm.NewStyle(pterm.FgRed)
+	p := pterm.DefaultInteractiveContinue.WithTextStyle(style)
+	testza.AssertEqual(t, p.TextStyle, style)
+}