From c933b9f710133940b4ac378d465275c493dfe67c Mon Sep 17 00:00:00 2001 From: Luis Davim Date: Tue, 26 Jul 2022 15:16:03 +0100 Subject: [PATCH] feat: adding interactive continue printer Signed-off-by: Luis Davim --- README.md | 31 ++- _examples/README.md | 25 +++ _examples/interactive_continue/README.md | 25 +++ _examples/interactive_continue/demo/README.md | 18 ++ .../interactive_continue/demo/animation.svg | 10 + _examples/interactive_continue/demo/ci.go | 19 ++ _examples/interactive_continue/demo/main.go | 11 ++ docs/README.md | 31 ++- interactive_continue_printer.go | 178 +++++++++++++++++ interactive_continue_printer_test.go | 183 ++++++++++++++++++ 10 files changed, 525 insertions(+), 6 deletions(-) create mode 100644 _examples/interactive_continue/README.md create mode 100644 _examples/interactive_continue/demo/README.md create mode 100644 _examples/interactive_continue/demo/animation.svg create mode 100644 _examples/interactive_continue/demo/ci.go create mode 100644 _examples/interactive_continue/demo/main.go create mode 100644 interactive_continue_printer.go create mode 100644 interactive_continue_printer_test.go diff --git a/README.md b/README.md index 7433dc04a..eb0acfee2 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.
**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.|
- + ### 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$| -
+ ## 🧪 Examples @@ -1066,6 +1066,31 @@ func boolToText(b bool) string { +### interactive_continue/demo + +![Animation](https://raw.githubusercontent.com/pterm/pterm/master/_examples/interactive_continue/demo/animation.svg) + +
+ +SHOW SOURCE + +```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) +} + +``` + +
+ ### interactive_multiselect/demo ![Animation](https://raw.githubusercontent.com/pterm/pterm/master/_examples/interactive_multiselect/demo/animation.svg) @@ -1608,7 +1633,7 @@ func main() { - + --- > GitHub [@pterm](https://github.com/pterm)  ·  diff --git a/_examples/README.md b/_examples/README.md index 12dad6280..41cc98480 100644 --- a/_examples/README.md +++ b/_examples/README.md @@ -938,6 +938,31 @@ func boolToText(b bool) string { +### interactive_continue/demo + +![Animation](https://raw.githubusercontent.com/pterm/pterm/master/_examples/interactive_continue/demo/animation.svg) + +
+ +SHOW SOURCE + +```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) +} + +``` + +
+ ### 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 000000000..3aa4b33a1 --- /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) + +
+ +SHOW SOURCE + +```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/README.md b/_examples/interactive_continue/demo/README.md new file mode 100644 index 000000000..f4c20783a --- /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 000000000..5ec0f59cd --- /dev/null +++ b/_examples/interactive_continue/demo/animation.svg @@ -0,0 +1,10 @@ +Doyouwanttocontinue[Y/n/a/s]: INFO Youanswered:yesRestartinganimation... diff --git a/_examples/interactive_continue/demo/ci.go b/_examples/interactive_continue/demo/ci.go new file mode 100644 index 000000000..2ba46081b --- /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 000000000..6684bdf90 --- /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 7433dc04a..eb0acfee2 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.
**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.|
- + ### 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$| -
+ ## 🧪 Examples @@ -1066,6 +1066,31 @@ func boolToText(b bool) string { +### interactive_continue/demo + +![Animation](https://raw.githubusercontent.com/pterm/pterm/master/_examples/interactive_continue/demo/animation.svg) + +
+ +SHOW SOURCE + +```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) +} + +``` + +
+ ### interactive_multiselect/demo ![Animation](https://raw.githubusercontent.com/pterm/pterm/master/_examples/interactive_multiselect/demo/animation.svg) @@ -1608,7 +1633,7 @@ func main() { - + --- > GitHub [@pterm](https://github.com/pterm)  ·  diff --git a/interactive_continue_printer.go b/interactive_continue_printer.go new file mode 100644 index 000000000..994e513b6 --- /dev/null +++ b/interactive_continue_printer.go @@ -0,0 +1,178 @@ +package pterm + +import ( + "fmt" + "os" + "strings" + + "atomicgo.dev/cursor" + "atomicgo.dev/keyboard" + "atomicgo.dev/keyboard/keys" +) + +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", "stop"}, + 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 + ShowFullHandles 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) { + panic("Invalid number of handles") + } + p.Handles = handles + return &p +} + +// WithFullHandles will set ShowFullHandles to true +// this makes the printer display the full options instead their shorthand version. +func (p InteractiveContinuePrinter) WithFullHandles() *InteractiveContinuePrinter { + p.ShowFullHandles = 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} + } + + if p.ShowFullHandles { + p.Handles = p.Options + } + + if p.Handles == nil || len(p.Handles) == 0 { + p.Handles = p.getDefaultHandles() + } + + 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.ShowFullHandles { + c = string([]rune(c)[0]) + } + if char == c || (i == p.DefaultValueIndex && strings.EqualFold(c, char)) { + Println() + result = p.Options[i] + return true, nil + } + } + case keys.Enter: + 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 +} + +// getDefaultHandles returns the short hand answers for the continueation prompt +func (p InteractiveContinuePrinter) getDefaultHandles() []string { + 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 +} + +// getSuffix returns the continueation prompt suffix +func (p InteractiveContinuePrinter) getSuffix() string { + if p.Handles == nil || len(p.Handles) != len(p.Options) { + panic("Handles not initialized") + } + + return p.SuffixStyle.Sprintf("[%s]", strings.Join(p.Handles, "/")) +} diff --git a/interactive_continue_printer_test.go b/interactive_continue_printer_test.go new file mode 100644 index 000000000..57e21edaf --- /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_WithFullHandles(t *testing.T) { + p := pterm.DefaultInteractiveContinue.WithFullHandles() + testza.AssertTrue(t, p.ShowFullHandles) + 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) +}