Repository: pterm/pterm (go)

Issue:
Add an option to change checkmarks in the interactive multiselect
I've run into an issue where Windows console isn't able to render the default multiselect's checkmarks ("✓" and "✗") due to them being undefined in the standard "Consolas" font.  I don't really feel like changing a font nor manually changing anything in the PTerm's source code. An option to define them would be a great addition to this awesome project.

Pull request description:
feat(multiselect): option to change checkmarks
### Description
Added the option to define the checkmarks in the interactive multiselect printer.
Also added some basic test cases.

### Scope
> What is affected by this pull request?

- [ ] Bug Fix
- [x] New Feature
- [x] Documentation
- [x] Other

### Related Issue
Fixes #418


### 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


Patch:
diff --git a/README.md b/README.md
index cfe4f34c..7d2beec9 100644
--- a/README.md
+++ b/README.md
@@ -1091,6 +1091,44 @@ func main() {
 
 </details>
 
+### interactive_multiselect/custom-checkmarks
+
+![Animation](https://raw.githubusercontent.com/pterm/pterm/master/_examples/interactive_multiselect/custom-checkmarks/animation.svg)
+
+<details>
+
+<summary>SHOW SOURCE</summary>
+
+```go
+package main
+
+import (
+	"fmt"
+
+	"atomicgo.dev/keyboard/keys"
+	"github.com/pterm/pterm"
+)
+
+func main() {
+	var options []string
+
+	for i := 0; i < 5; i++ {
+		options = append(options, fmt.Sprintf("Option %d", i))
+	}
+
+	printer := pterm.DefaultInteractiveMultiselect.WithOptions(options)
+	printer.Filter = false
+	printer.KeyConfirm = keys.Enter
+	printer.KeySelect = keys.Space
+	printer.Checkmark = pterm.Checkmark{Checked: "+", Unchecked: "-"}
+	selectedOptions, _ := printer.Show()
+	pterm.Info.Printfln("Selected options: %s", pterm.Green(selectedOptions))
+}
+
+```
+
+</details>
+
 ### interactive_multiselect/custom-keys
 
 ![Animation](https://raw.githubusercontent.com/pterm/pterm/master/_examples/interactive_multiselect/custom-keys/animation.svg)
diff --git a/_examples/interactive_multiselect/README.md b/_examples/interactive_multiselect/README.md
index 8e9d4ce1..981301f6 100644
--- a/_examples/interactive_multiselect/README.md
+++ b/_examples/interactive_multiselect/README.md
@@ -1,3 +1,41 @@
+### interactive_multiselect/custom-checkmarks
+
+![Animation](https://raw.githubusercontent.com/pterm/pterm/master/_examples/interactive_multiselect/custom-checkmarks/animation.svg)
+
+<details>
+
+<summary>SHOW SOURCE</summary>
+
+```go
+package main
+
+import (
+	"fmt"
+
+	"atomicgo.dev/keyboard/keys"
+	"github.com/pterm/pterm"
+)
+
+func main() {
+	var options []string
+
+	for i := 0; i < 5; i++ {
+		options = append(options, fmt.Sprintf("Option %d", i))
+	}
+
+	printer := pterm.DefaultInteractiveMultiselect.WithOptions(options)
+	printer.Filter = false
+	printer.KeyConfirm = keys.Enter
+	printer.KeySelect = keys.Space
+	printer.Checkmark = pterm.Checkmark{Checked: "+", Unchecked: "-"}
+	selectedOptions, _ := printer.Show()
+	pterm.Info.Printfln("Selected options: %s", pterm.Green(selectedOptions))
+}
+
+```
+
+</details>
+
 ### interactive_multiselect/custom-keys
 
 ![Animation](https://raw.githubusercontent.com/pterm/pterm/master/_examples/interactive_multiselect/custom-keys/animation.svg)
diff --git a/_examples/interactive_multiselect/custom-checkmarks/README.md b/_examples/interactive_multiselect/custom-checkmarks/README.md
new file mode 100644
index 00000000..050c56d1
--- /dev/null
+++ b/_examples/interactive_multiselect/custom-checkmarks/README.md
@@ -0,0 +1,32 @@
+# interactive_multiselect/custom-checkmark
+
+![Animation](animation.svg)
+
+```go
+package main
+
+import (
+	"fmt"
+
+	"atomicgo.dev/keyboard/keys"
+
+	"github.com/pterm/pterm"
+)
+
+func main() {
+	var options []string
+
+	for i := 0; i < 5; i++ {
+		options = append(options, fmt.Sprintf("Option %d", i))
+	}
+
+	printer := pterm.DefaultInteractiveMultiselect.WithOptions(options)
+	printer.Filter = false
+	printer.KeyConfirm = keys.Enter
+	printer.KeySelect = keys.Space
+	printer.Checkmark = pterm.Checkmark{Checked: "+", Unchecked: "-"}
+	selectedOptions, _ := printer.Show()
+	pterm.Info.Printfln("Selected options: %s", pterm.Green(selectedOptions))
+}
+
+```
diff --git a/_examples/interactive_multiselect/custom-checkmarks/ci.go b/_examples/interactive_multiselect/custom-checkmarks/ci.go
new file mode 100644
index 00000000..f8e4cd08
--- /dev/null
+++ b/_examples/interactive_multiselect/custom-checkmarks/ci.go
@@ -0,0 +1,44 @@
+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)
+			for i := 0; i < 10; i++ {
+				keyboard.SimulateKeyPress(keys.Down)
+				if i%2 == 0 {
+					time.Sleep(time.Millisecond * 100)
+					keyboard.SimulateKeyPress(keys.Enter)
+				}
+				time.Sleep(time.Millisecond * 500)
+			}
+			time.Sleep(time.Second)
+
+			for _, s := range "fuzzy" {
+				keyboard.SimulateKeyPress(s)
+				time.Sleep(time.Millisecond * 150)
+			}
+
+			time.Sleep(time.Second)
+
+			for i := 0; i < 2; i++ {
+				keyboard.SimulateKeyPress(keys.Down)
+				time.Sleep(time.Millisecond * 300)
+			}
+
+			keyboard.SimulateKeyPress(keys.Enter)
+			time.Sleep(time.Millisecond * 350)
+			keyboard.SimulateKeyPress(keys.Tab)
+		}()
+	}
+}
diff --git a/_examples/interactive_multiselect/custom-checkmarks/main.go b/_examples/interactive_multiselect/custom-checkmarks/main.go
new file mode 100644
index 00000000..aeebaed7
--- /dev/null
+++ b/_examples/interactive_multiselect/custom-checkmarks/main.go
@@ -0,0 +1,25 @@
+package main
+
+import (
+	"fmt"
+
+	"atomicgo.dev/keyboard/keys"
+
+	"github.com/pterm/pterm"
+)
+
+func main() {
+	var options []string
+
+	for i := 0; i < 5; i++ {
+		options = append(options, fmt.Sprintf("Option %d", i))
+	}
+
+	printer := pterm.DefaultInteractiveMultiselect.WithOptions(options)
+	printer.Filter = false
+	printer.KeyConfirm = keys.Enter
+	printer.KeySelect = keys.Space
+	printer.Checkmark = pterm.Checkmark{Checked: "+", Unchecked: "-"}
+	selectedOptions, _ := printer.Show()
+	pterm.Info.Printfln("Selected options: %s", pterm.Green(selectedOptions))
+}
diff --git a/atoms.go b/atoms.go
index d2410923..b4eb0b09 100644
--- a/atoms.go
+++ b/atoms.go
@@ -11,6 +11,12 @@ type Bar struct {
 	LabelStyle *Style
 }
 
+// Checkmark is used in the interactive multiselect printer.
+type Checkmark struct {
+	Checked   string
+	Unchecked string
+}
+
 // WithLabel returns a new Bar with a specific option.
 func (p Bar) WithLabel(s string) *Bar {
 	p.Label = s
diff --git a/interactive_multiselect_printer.go b/interactive_multiselect_printer.go
index dc6be6b2..5f9a63af 100644
--- a/interactive_multiselect_printer.go
+++ b/interactive_multiselect_printer.go
@@ -8,6 +8,7 @@ import (
 	"atomicgo.dev/keyboard"
 	"atomicgo.dev/keyboard/keys"
 	"github.com/lithammer/fuzzysearch/fuzzy"
+
 	"github.com/pterm/pterm/internal"
 )
 
@@ -25,6 +26,10 @@ var (
 		Filter:         true,
 		KeySelect:      keys.Enter,
 		KeyConfirm:     keys.Tab,
+		Checkmark: Checkmark{
+			Checked:   "✓",
+			Unchecked: "✗",
+		},
 	}
 )
 
@@ -39,6 +44,7 @@ type InteractiveMultiselectPrinter struct {
 	Selector       string
 	SelectorStyle  *Style
 	Filter         bool
+	Checkmark      Checkmark
 
 	selectedOption        int
 	selectedOptions       []int
@@ -95,6 +101,12 @@ func (p InteractiveMultiselectPrinter) WithKeyConfirm(keyConfirm keys.KeyCode) *
 	return &p
 }
 
+// WithCheckmark sets the checkmark
+func (p InteractiveMultiselectPrinter) WithCheckmark(checkmark Checkmark) *InteractiveMultiselectPrinter {
+	p.Checkmark = checkmark
+	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
@@ -345,9 +357,9 @@ func (p *InteractiveMultiselectPrinter) renderSelectMenu() string {
 		}
 		var checkmark string
 		if p.isSelected(option) {
-			checkmark = fmt.Sprintf("[%s]", Green("✓"))
+			checkmark = fmt.Sprintf("[%s]", Green(p.Checkmark.Checked))
 		} else {
-			checkmark = fmt.Sprintf("[%s]", Red("✗"))
+			checkmark = fmt.Sprintf("[%s]", Red(p.Checkmark.Unchecked))
 		}
 		if i == p.selectedOption {
 			content += Sprintf("%s %s %s\n", p.renderSelector(), checkmark, option)


Test patch:
diff --git a/interactive_multiselect_printer_test.go b/interactive_multiselect_printer_test.go
new file mode 100644
index 00000000..8f8cc030
--- /dev/null
+++ b/interactive_multiselect_printer_test.go
@@ -0,0 +1,68 @@
+package pterm_test
+
+import (
+	"testing"
+
+	"atomicgo.dev/keyboard"
+	"atomicgo.dev/keyboard/keys"
+	"github.com/MarvinJWendt/testza"
+
+	"github.com/pterm/pterm"
+)
+
+func TestInteractiveMultiselectPrinter_Show(t *testing.T) {
+	go func() {
+		keyboard.SimulateKeyPress(keys.Down)
+		keyboard.SimulateKeyPress(keys.Down)
+		keyboard.SimulateKeyPress(keys.Enter)
+		keyboard.SimulateKeyPress(keys.Tab)
+	}()
+	result, _ := pterm.DefaultInteractiveMultiselect.WithOptions([]string{"a", "b", "c", "d", "e"}).WithDefaultOptions([]string{"b"}).Show()
+	testza.AssertEqual(t, []string{"b", "c"}, result)
+}
+
+func TestInteractiveMultiselectPrinter_Show_MaxHeightSlidingWindow(t *testing.T) {
+	go func() {
+		keyboard.SimulateKeyPress(keys.Up)
+		keyboard.SimulateKeyPress(keys.Up)
+		keyboard.SimulateKeyPress(keys.Enter)
+		keyboard.SimulateKeyPress(keys.Tab)
+	}()
+	result, _ := pterm.DefaultInteractiveMultiselect.WithOptions([]string{"a", "b", "c", "d", "e", "f"}).WithDefaultOptions([]string{"b"}).Show()
+	testza.AssertEqual(t, []string{"b", "e"}, result)
+}
+
+func TestInteractiveMultiselectPrinter_WithDefaultText(t *testing.T) {
+	p := pterm.DefaultInteractiveMultiselect.WithDefaultText("default")
+	testza.AssertEqual(t, p.DefaultText, "default")
+}
+
+func TestInteractiveMultiselectPrinter_WithDefaultOption(t *testing.T) {
+	p := pterm.DefaultInteractiveMultiselect.WithDefaultOptions([]string{"default"})
+	testza.AssertEqual(t, p.DefaultOptions, []string{"default"})
+}
+
+func TestInteractiveMultiselectPrinter_WithOptions(t *testing.T) {
+	p := pterm.DefaultInteractiveMultiselect.WithOptions([]string{"a", "b", "c"})
+	testza.AssertEqual(t, p.Options, []string{"a", "b", "c"})
+}
+
+func TestInteractiveMultiselectPrinter_WithMaxHeight(t *testing.T) {
+	p := pterm.DefaultInteractiveMultiselect.WithMaxHeight(1337)
+	testza.AssertEqual(t, p.MaxHeight, 1337)
+}
+
+func TestInteractiveMultiselectPrinter_WithKeySelect(t *testing.T) {
+	p := pterm.DefaultInteractiveMultiselect.WithKeySelect(keys.Left).WithOptions([]string{"a", "b", "c"})
+	testza.AssertEqual(t, p.KeySelect, keys.Left)
+}
+
+func TestInteractiveMultiselectPrinter_WithKeyConfirm(t *testing.T) {
+	p := pterm.DefaultInteractiveMultiselect.WithKeyConfirm(keys.Left).WithOptions([]string{"a", "b", "c"})
+	testza.AssertEqual(t, p.KeyConfirm, keys.Left)
+}
+
+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: "-"})
+}