Repository: pterm/pterm
Pull request #458 (2023-03-21 18:20:17)

feat: add optional mask to InteractiveTextInputPrinter for prompting for secrets/passwords/tokens
### Description
Adds `WithMask(string)` to `TestInteractiveTextInputPrinter` for prompting for secrets (such as passwords, API tokens, etc.)
It gives user feedback by showing `mask` characters instead of the actual characters.

Inspired by the [password](https://github.com/go-survey/survey#password) prompter from [go-survey/survey](https://github.com/go-survey/survey)

### Example

```go
result, _ := pterm.DefaultInteractiveTextInput.WithMask("*").Show()
```

### 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
- [ ] I updated the examples to fit with my changes
- [x] I have added tests for my newly created methods


MarvinJWendt: Hi @MarkusZoppelt, thanks for the PR!
I think we don't need a new printer for that. We have the `InteractiveTextInput`, and we could implement options there to mask the input. This would make it easier to maintain and to configure. 
Simple options like `HideInput (boolean)` and `InputMask (string)` could be used to configure the `InteractiveTextInput` to accept passwords.
MarkusZoppelt: > We have the `InteractiveTextInput`, and we could implement options there to mask the input. This would make it easier to maintain and to configure.

@MarkusZoppelt I see, makes sense. I will will take some time and modify this PR to include the changes in `InteractiveTextInput` instead of a new printer.
snakeice: @MarkusZoppelt I help you :sweat_smile:, I need this feature -> https://github.com/MarkusZoppelt/pterm/pull/1
codecov[bot]: ## [Codecov](https://codecov.io/gh/pterm/pterm/pull/458?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pterm) Report
> Merging [#458](https://codecov.io/gh/pterm/pterm/pull/458?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pterm) (3487b25) into [master](https://codecov.io/gh/pterm/pterm/commit/8b56f53165fef8dedfce963ddcec70fbcc575cad?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pterm) (8b56f53) will **decrease** coverage by `0.07%`.
> The diff coverage is `50.00%`.

```diff
@@            Coverage Diff             @@
##           master     #458      +/-   ##
==========================================
- Coverage   85.90%   85.83%   -0.07%     
==========================================
  Files          29       29              
  Lines        2135     2139       +4     
==========================================
+ Hits         1834     1836       +2     
- Misses        266      268       +2     
  Partials       35       35              
```


| [Impacted Files](https://codecov.io/gh/pterm/pterm/pull/458?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pterm) | Coverage Δ | |
|---|---|---|
| [interactive\_textinput\_printer.go](https://codecov.io/gh/pterm/pterm/pull/458?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pterm#diff-aW50ZXJhY3RpdmVfdGV4dGlucHV0X3ByaW50ZXIuZ28=) | `6.10% <50.00%> (+1.38%)` | :arrow_up: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pterm). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pterm)

MarvinJWendt: Hi, please include a test case that covers the added line:
![image](https://user-images.githubusercontent.com/31022056/227380113-9d93af01-f86c-497c-9e0f-f7d49a5f4387.png)

MarkusZoppelt: @MarvinJWendt Is something else missing before this can be merged? :) 
MarvinJWendt: Hi @MarkusZoppelt, I will review it in the next few days, I am currently a little busy :)

Patch:
diff --git a/_examples/interactive_textinput/password/ci.go b/_examples/interactive_textinput/password/ci.go
new file mode 100644
index 00000000..b238cc9e
--- /dev/null
+++ b/_examples/interactive_textinput/password/ci.go
@@ -0,0 +1,30 @@
+package main
+
+import (
+	"os"
+	"time"
+
+	"atomicgo.dev/keyboard"
+	"atomicgo.dev/keyboard/keys"
+)
+
+// ------ 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)
+			input := "Hello, World!"
+			for _, r := range input {
+				if r == '\n' {
+					keyboard.SimulateKeyPress(keys.Enter)
+				} else {
+					keyboard.SimulateKeyPress(r)
+				}
+				time.Sleep(time.Millisecond * 250)
+			}
+
+			keyboard.SimulateKeyPress(keys.Enter)
+		}()
+	}
+}
diff --git a/_examples/interactive_textinput/password/main.go b/_examples/interactive_textinput/password/main.go
new file mode 100644
index 00000000..f8eb0c3c
--- /dev/null
+++ b/_examples/interactive_textinput/password/main.go
@@ -0,0 +1,10 @@
+package main
+
+import "github.com/pterm/pterm"
+
+func main() {
+	result, _ := pterm.DefaultInteractiveTextInput.WithMask("*").Show("Enter your password")
+
+	logger := pterm.DefaultLogger
+	logger.Info("Password received", logger.Args("password", result))
+}
diff --git a/interactive_textinput_printer.go b/interactive_textinput_printer.go
index e7174555..512e24f9 100644
--- a/interactive_textinput_printer.go
+++ b/interactive_textinput_printer.go
@@ -15,6 +15,7 @@ var (
 	DefaultInteractiveTextInput = InteractiveTextInputPrinter{
 		DefaultText: "Input text",
 		TextStyle:   &ThemeDefault.PrimaryStyle,
+		Mask:        "",
 	}
 )
 
@@ -23,6 +24,7 @@ type InteractiveTextInputPrinter struct {
 	TextStyle   *Style
 	DefaultText string
 	MultiLine   bool
+	Mask        string
 
 	input      []string
 	cursorXPos int
@@ -48,6 +50,12 @@ func (p InteractiveTextInputPrinter) WithMultiLine(multiLine ...bool) *Interacti
 	return &p
 }
 
+// WithMask sets the mask.
+func (p InteractiveTextInputPrinter) WithMask(mask string) *InteractiveTextInputPrinter {
+	p.Mask = mask
+	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
@@ -200,6 +208,7 @@ func (p InteractiveTextInputPrinter) updateArea(area *AreaPrinter) string {
 		p.cursorYPos = 0
 	}
 	areaText := p.text
+
 	for i, s := range p.input {
 		if i < len(p.input)-1 {
 			areaText += s + "\n"
@@ -207,6 +216,11 @@ func (p InteractiveTextInputPrinter) updateArea(area *AreaPrinter) string {
 			areaText += s
 		}
 	}
+
+	if p.Mask != "" {
+		areaText = p.text + strings.Repeat(p.Mask, internal.GetStringMaxWidth(areaText)-internal.GetStringMaxWidth(p.text))
+	}
+
 	if p.cursorXPos+internal.GetStringMaxWidth(p.input[p.cursorYPos]) < 1 {
 		p.cursorXPos = -internal.GetStringMaxWidth(p.input[p.cursorYPos])
 	}


Test patch:
diff --git a/interactive_textinput_printer_test.go b/interactive_textinput_printer_test.go
index 1b0d9523..a85fe338 100644
--- a/interactive_textinput_printer_test.go
+++ b/interactive_textinput_printer_test.go
@@ -3,6 +3,8 @@ package pterm_test
 import (
 	"testing"
 
+	"atomicgo.dev/keyboard"
+	"atomicgo.dev/keyboard/keys"
 	"github.com/MarvinJWendt/testza"
 
 	"github.com/pterm/pterm"
@@ -28,3 +30,14 @@ func TestInteractiveTextInputPrinter_WithTextStyle(t *testing.T) {
 	p := pterm.DefaultInteractiveTextInput.WithTextStyle(style)
 	testza.AssertEqual(t, p.TextStyle, style)
 }
+
+func TestInteractiveTextInputPrinter_WithMask(t *testing.T) {
+	go func() {
+		keyboard.SimulateKeyPress('a')
+		keyboard.SimulateKeyPress('b')
+		keyboard.SimulateKeyPress('c')
+		keyboard.SimulateKeyPress(keys.Enter)
+	}()
+	result, _ := pterm.DefaultInteractiveTextInput.WithMask("*").Show()
+	testza.AssertEqual(t, result, "abc")
+}