From 1200d369eddc2d15861d46b5b1035d3461c4e859 Mon Sep 17 00:00:00 2001 From: Marvin Wendt Date: Thu, 2 Jun 2022 11:37:19 +0200 Subject: [PATCH 01/25] added `InteractiveConfirmPrinter` printer --- color.go | 12 +-- go.mod | 1 + go.sum | 5 ++ interactive_confirm_printer.go | 136 +++++++++++++++++++++++++++++++++ theme.go | 2 +- 5 files changed, 149 insertions(+), 7 deletions(-) create mode 100644 interactive_confirm_printer.go diff --git a/color.go b/color.go index b1ef19b8b..3626ee475 100644 --- a/color.go +++ b/color.go @@ -212,32 +212,32 @@ func (c Color) Printfln(format string, a ...interface{}) *TextPrinter { // PrintOnError prints every error which is not nil. // If every error is nil, nothing will be printed. // This can be used for simple error checking. -func (p Color) PrintOnError(a ...interface{}) *TextPrinter { +func (c Color) PrintOnError(a ...interface{}) *TextPrinter { for _, arg := range a { if err, ok := arg.(error); ok { if err != nil { - p.Println(err) + c.Println(err) } } } - tp := TextPrinter(p) + tp := TextPrinter(c) return &tp } // PrintOnErrorf wraps every error which is not nil and prints it. // If every error is nil, nothing will be printed. // This can be used for simple error checking. -func (p Color) PrintOnErrorf(format string, a ...interface{}) *TextPrinter { +func (c Color) PrintOnErrorf(format string, a ...interface{}) *TextPrinter { for _, arg := range a { if err, ok := arg.(error); ok { if err != nil { - p.Println(fmt.Errorf(format, err)) + c.Println(fmt.Errorf(format, err)) } } } - tp := TextPrinter(p) + tp := TextPrinter(c) return &tp } diff --git a/go.mod b/go.mod index 58dbc73cd..6800ba450 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/pterm/pterm go 1.15 require ( + atomicgo.dev/keyboard v0.1.0 // indirect github.com/MarvinJWendt/testza v0.3.5 github.com/atomicgo/cursor v0.0.1 github.com/gookit/color v1.5.0 diff --git a/go.sum b/go.sum index 41ce65452..91ec3f1d0 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +atomicgo.dev/keyboard v0.1.0 h1:KbkX1WD7QDZmhTl5GGrc4HTLeptbGZP5MGovklvzvAo= +atomicgo.dev/keyboard v0.1.0/go.mod h1:BRn8gWyv0+lWhrYvSv4HX1eK6VOk2euOAMV1cv0fOZ4= github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs= github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8= github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII= @@ -8,6 +10,8 @@ github.com/MarvinJWendt/testza v0.3.5 h1:g9krITRRlIsF1eO9sUKXtiTw670gZIIk6T08Kee github.com/MarvinJWendt/testza v0.3.5/go.mod h1:ExbTpWmA1z2E9HSskvrNcwApoX4F9bID692s10nuHRY= github.com/atomicgo/cursor v0.0.1 h1:xdogsqa6YYlLfM+GyClC/Lchf7aiMerFiZQn7soTOoU= github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= +github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= +github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -46,6 +50,7 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8= github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/interactive_confirm_printer.go b/interactive_confirm_printer.go new file mode 100644 index 000000000..1c6cd9b5a --- /dev/null +++ b/interactive_confirm_printer.go @@ -0,0 +1,136 @@ +package pterm + +import ( + "fmt" + "os" + + "atomicgo.dev/keyboard" + "atomicgo.dev/keyboard/keys" +) + +var ( + // DefaultInteractiveConfirm is the default InteractiveConfirm printer. + DefaultInteractiveConfirm = InteractiveConfirmPrinter{ + DefaultValue: false, + TextStyle: &ThemeDefault.PrimaryStyle, + ConfirmText: "Yes", + ConfirmStyle: &ThemeDefault.SuccessMessageStyle, + RejectText: "No", + RejectStyle: &ThemeDefault.ErrorMessageStyle, + SuffixStyle: &ThemeDefault.SecondaryStyle, + } +) + +type InteractiveConfirmPrinter struct { + DefaultValue bool + TextStyle *Style + ConfirmText string + ConfirmStyle *Style + RejectText string + RejectStyle *Style + SuffixStyle *Style +} + +// WithDefaultValue sets the default value, which will be returned when the user presses enter without typing "y" or "n". +func (p InteractiveConfirmPrinter) WithDefaultValue(value bool) *InteractiveConfirmPrinter { + p.DefaultValue = value + return &p +} + +// WithTextStyle sets the text style. +func (p InteractiveConfirmPrinter) WithTextStyle(style *Style) *InteractiveConfirmPrinter { + p.TextStyle = style + return &p +} + +// WithConfirmText sets the confirm text. +func (p InteractiveConfirmPrinter) WithConfirmText(text string) *InteractiveConfirmPrinter { + p.ConfirmText = text + return &p +} + +// WithConfirmStyle sets the confirm style. +func (p InteractiveConfirmPrinter) WithConfirmStyle(style *Style) *InteractiveConfirmPrinter { + p.ConfirmStyle = style + return &p +} + +// WithRejectText sets the reject text. +func (p InteractiveConfirmPrinter) WithRejectText(text string) *InteractiveConfirmPrinter { + p.RejectText = text + return &p +} + +// WithRejectStyle sets the reject style. +func (p InteractiveConfirmPrinter) WithRejectStyle(style *Style) *InteractiveConfirmPrinter { + p.RejectStyle = style + return &p +} + +// WithSuffixStyle sets the suffix style. +func (p InteractiveConfirmPrinter) WithSuffixStyle(style *Style) *InteractiveConfirmPrinter { + p.SuffixStyle = style + return &p +} + +// Show shows the confirm prompt. +// +// Example: +// result, _ := pterm.DefaultInteractiveConfirm.Show("Are you sure?") +// pterm.Println(result) +func (p *InteractiveConfirmPrinter) Show(text string) (bool, error) { + err := keyboard.StartListener() + if err != nil { + return false, fmt.Errorf("failed to start keyboard listener: %w", err) + } + + p.TextStyle.Print(text + " " + p.getSuffix() + ": ") + + for { + keyInfo, err := keyboard.GetKey() + key := keyInfo.Code + char := keyInfo.String() + if err != nil { + return false, fmt.Errorf("failed to get key: %w", err) + } + + switch key { + case keys.RuneKey: + switch char { + case "y", "Y": + p.ConfirmStyle.Print(p.ConfirmText) + Println() + return true, keyboard.StopListener() + case "n", "N": + p.RejectStyle.Print(p.RejectText) + Println() + return false, keyboard.StopListener() + } + case keys.Enter: + if p.DefaultValue { + p.ConfirmStyle.Print(p.ConfirmText) + } else { + p.RejectStyle.Print(p.RejectText) + } + Println() + return p.DefaultValue, keyboard.StopListener() + case keys.CtrlC: + os.Exit(1) + return false, keyboard.StopListener() + } + } +} + +func (p InteractiveConfirmPrinter) getSuffix() string { + var y string + var n string + if p.DefaultValue { + y = "Y" + n = "n" + } else { + y = "y" + n = "N" + } + + return p.SuffixStyle.Sprintf("[%s/%s]", y, n) +} diff --git a/theme.go b/theme.go index 9418642cf..379140b3a 100644 --- a/theme.go +++ b/theme.go @@ -4,7 +4,7 @@ var ( // ThemeDefault is the default theme used by PTerm. // If this variable is overwritten, the new value is used as default theme. ThemeDefault = Theme{ - PrimaryStyle: Style{FgCyan}, + PrimaryStyle: Style{FgLightCyan}, SecondaryStyle: Style{FgLightMagenta}, HighlightStyle: Style{Bold, FgYellow}, InfoMessageStyle: Style{FgLightCyan}, From 5eb30b35bd4d99ec6f0ff5e90c5ec1b6516b6a0f Mon Sep 17 00:00:00 2001 From: Marvin Wendt Date: Thu, 2 Jun 2022 14:24:44 +0200 Subject: [PATCH 02/25] added `InteractiveSelectPrinter` printer --- interactive_confirm_printer.go | 8 ++- interactive_select_printer.go | 106 +++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 interactive_select_printer.go diff --git a/interactive_confirm_printer.go b/interactive_confirm_printer.go index 1c6cd9b5a..5e0997b64 100644 --- a/interactive_confirm_printer.go +++ b/interactive_confirm_printer.go @@ -78,13 +78,17 @@ func (p InteractiveConfirmPrinter) WithSuffixStyle(style *Style) *InteractiveCon // Example: // result, _ := pterm.DefaultInteractiveConfirm.Show("Are you sure?") // pterm.Println(result) -func (p *InteractiveConfirmPrinter) Show(text string) (bool, error) { +func (p InteractiveConfirmPrinter) Show(text ...string) (bool, error) { err := keyboard.StartListener() if err != nil { return false, fmt.Errorf("failed to start keyboard listener: %w", err) } - p.TextStyle.Print(text + " " + p.getSuffix() + ": ") + if text == nil { + text = []string{"Please confirm"} + } + + p.TextStyle.Print(text[0] + " " + p.getSuffix() + ": ") for { keyInfo, err := keyboard.GetKey() diff --git a/interactive_select_printer.go b/interactive_select_printer.go new file mode 100644 index 000000000..36b7c27aa --- /dev/null +++ b/interactive_select_printer.go @@ -0,0 +1,106 @@ +package pterm + +import ( + "fmt" + "os" + + "atomicgo.dev/keyboard" + "atomicgo.dev/keyboard/keys" +) + +var ( + // DefaultInteractiveSelect is the default InteractiveSelect printer. + DefaultInteractiveSelect = InteractiveSelectPrinter{} +) + +type InteractiveSelectPrinter struct { + Options []string + DefaultOption string + MaxHeight int + + selectedOption int + result string +} + +// WithOptions sets the options. +func (p InteractiveSelectPrinter) WithOptions(options []string) *InteractiveSelectPrinter { + p.Options = options + return &p +} + +// WithDefaultOption sets the default options. +func (p InteractiveSelectPrinter) WithDefaultOption(option string) *InteractiveSelectPrinter { + p.DefaultOption = option + return &p +} + +func (p InteractiveSelectPrinter) Show(text ...string) (string, error) { + err := keyboard.StartListener() + if err != nil { + return "", fmt.Errorf("failed to start keyboard listener: %w", err) + } + + if text == nil { + text = []string{""} + } + + // Get index of default option + if p.DefaultOption != "" { + for i, option := range p.Options { + if option == p.DefaultOption { + p.selectedOption = i + break + } + } + } + + area, err := DefaultArea.Start(p.renderSelectMenu()) + if err != nil { + return "", fmt.Errorf("could not start area: %w", err) + } + + for p.result == "" { + keyInfo, err := keyboard.GetKey() + if err != nil { + return "", err + } + key := keyInfo.Code + + switch key { + case keys.Up: + if p.selectedOption > 0 { + p.selectedOption-- + } + area.Update(p.renderSelectMenu()) + case keys.Down: + if p.selectedOption < len(p.Options)-1 { + p.selectedOption++ + } + area.Update(p.renderSelectMenu()) + case keys.CtrlC: + os.Exit(1) + case keys.Enter: + p.result = p.Options[p.selectedOption] + } + } + + err = keyboard.StopListener() + if err != nil { + return "", fmt.Errorf("failed to start keyboard listener: %w", err) + } + + return p.result, nil +} + +func (p InteractiveSelectPrinter) renderSelectMenu() string { + var content string + for i, option := range p.Options { + if i == p.selectedOption { + content += fmt.Sprintf("> %s\n", option) + } else { + content += fmt.Sprintf(" %s\n", option) + } + } + + return content +} From 6355fa9069caf8cdfb43fe0ce23cfdcd4e22419f Mon Sep 17 00:00:00 2001 From: Marvin Wendt Date: Fri, 3 Jun 2022 10:07:29 +0200 Subject: [PATCH 03/25] updated `atomicgo.dev/cursor` and `atomicgo.dev/keyboard` --- area_printer.go | 2 +- go.mod | 4 +- go.sum | 7 +-- interactive_confirm_printer.go | 79 ++++++++++++++++++---------------- interactive_select_printer.go | 74 ++++++++++++++----------------- 5 files changed, 82 insertions(+), 84 deletions(-) diff --git a/area_printer.go b/area_printer.go index d30410363..9fe5a7197 100644 --- a/area_printer.go +++ b/area_printer.go @@ -3,7 +3,7 @@ package pterm import ( "strings" - "github.com/atomicgo/cursor" + "atomicgo.dev/cursor" "github.com/pterm/pterm/internal" ) diff --git a/go.mod b/go.mod index 6800ba450..f5624bb62 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,9 @@ module github.com/pterm/pterm go 1.15 require ( - atomicgo.dev/keyboard v0.1.0 // indirect + atomicgo.dev/cursor v0.1.0 + atomicgo.dev/keyboard v0.2.0 github.com/MarvinJWendt/testza v0.3.5 - github.com/atomicgo/cursor v0.0.1 github.com/gookit/color v1.5.0 github.com/mattn/go-runewidth v0.0.13 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 diff --git a/go.sum b/go.sum index 91ec3f1d0..4b64c8653 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ -atomicgo.dev/keyboard v0.1.0 h1:KbkX1WD7QDZmhTl5GGrc4HTLeptbGZP5MGovklvzvAo= -atomicgo.dev/keyboard v0.1.0/go.mod h1:BRn8gWyv0+lWhrYvSv4HX1eK6VOk2euOAMV1cv0fOZ4= +atomicgo.dev/cursor v0.1.0 h1:hsNUAMs7ioxwHLW03XDoO3+5heMfP8tJsiW14XIKlkw= +atomicgo.dev/cursor v0.1.0/go.mod h1:Lr4ZJB3U7DfPPOkbH7/6TOtJ4vFGHlgj1nc+n900IpU= +atomicgo.dev/keyboard v0.2.0 h1:sn0NUNl7l8PHJAsoWEvdGiMsOTSpOVSDlyMI+9tjf6o= +atomicgo.dev/keyboard v0.2.0/go.mod h1:BRn8gWyv0+lWhrYvSv4HX1eK6VOk2euOAMV1cv0fOZ4= github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs= github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8= github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII= @@ -8,7 +10,6 @@ github.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzX github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/2oUqKc6bF2c= github.com/MarvinJWendt/testza v0.3.5 h1:g9krITRRlIsF1eO9sUKXtiTw670gZIIk6T08Keeo1nM= github.com/MarvinJWendt/testza v0.3.5/go.mod h1:ExbTpWmA1z2E9HSskvrNcwApoX4F9bID692s10nuHRY= -github.com/atomicgo/cursor v0.0.1 h1:xdogsqa6YYlLfM+GyClC/Lchf7aiMerFiZQn7soTOoU= github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= diff --git a/interactive_confirm_printer.go b/interactive_confirm_printer.go index 5e0997b64..362ca3e59 100644 --- a/interactive_confirm_printer.go +++ b/interactive_confirm_printer.go @@ -79,50 +79,55 @@ func (p InteractiveConfirmPrinter) WithSuffixStyle(style *Style) *InteractiveCon // result, _ := pterm.DefaultInteractiveConfirm.Show("Are you sure?") // pterm.Println(result) func (p InteractiveConfirmPrinter) Show(text ...string) (bool, error) { - err := keyboard.StartListener() - if err != nil { - return false, fmt.Errorf("failed to start keyboard listener: %w", err) - } + var result bool + err := keyboard.Listen(func(keyInfo keys.Key) (stop bool, err error) { + if err != nil { + return false, fmt.Errorf("failed to start keyboard listener: %w", err) + } - if text == nil { - text = []string{"Please confirm"} - } + if text == nil { + text = []string{"Please confirm"} + } - p.TextStyle.Print(text[0] + " " + p.getSuffix() + ": ") + p.TextStyle.Print(text[0] + " " + p.getSuffix() + ": ") - for { - keyInfo, err := keyboard.GetKey() - key := keyInfo.Code - char := keyInfo.String() - if err != nil { - return false, fmt.Errorf("failed to get key: %w", err) - } + for { + key := keyInfo.Code + char := keyInfo.String() + if err != nil { + return false, fmt.Errorf("failed to get key: %w", err) + } - switch key { - case keys.RuneKey: - switch char { - case "y", "Y": - p.ConfirmStyle.Print(p.ConfirmText) - Println() - return true, keyboard.StopListener() - case "n", "N": - p.RejectStyle.Print(p.RejectText) + switch key { + case keys.RuneKey: + switch char { + case "y", "Y": + p.ConfirmStyle.Print(p.ConfirmText) + Println() + result = true + return true, nil + case "n", "N": + p.RejectStyle.Print(p.RejectText) + Println() + result = false + return true, nil + } + case keys.Enter: + if p.DefaultValue { + p.ConfirmStyle.Print(p.ConfirmText) + } else { + p.RejectStyle.Print(p.RejectText) + } Println() - return false, keyboard.StopListener() + result = p.DefaultValue + return true, nil + case keys.CtrlC: + os.Exit(1) + return true, nil } - case keys.Enter: - if p.DefaultValue { - p.ConfirmStyle.Print(p.ConfirmText) - } else { - p.RejectStyle.Print(p.RejectText) - } - Println() - return p.DefaultValue, keyboard.StopListener() - case keys.CtrlC: - os.Exit(1) - return false, keyboard.StopListener() } - } + }) + return result, err } func (p InteractiveConfirmPrinter) getSuffix() string { diff --git a/interactive_select_printer.go b/interactive_select_printer.go index 36b7c27aa..0fed80bb7 100644 --- a/interactive_select_printer.go +++ b/interactive_select_printer.go @@ -35,56 +35,48 @@ func (p InteractiveSelectPrinter) WithDefaultOption(option string) *InteractiveS } func (p InteractiveSelectPrinter) Show(text ...string) (string, error) { - err := keyboard.StartListener() - if err != nil { - return "", fmt.Errorf("failed to start keyboard listener: %w", err) - } - - if text == nil { - text = []string{""} - } + err := keyboard.Listen(func(keyInfo keys.Key) (stop bool, err error) { + if text == nil { + text = []string{""} + } - // Get index of default option - if p.DefaultOption != "" { - for i, option := range p.Options { - if option == p.DefaultOption { - p.selectedOption = i - break + // Get index of default option + if p.DefaultOption != "" { + for i, option := range p.Options { + if option == p.DefaultOption { + p.selectedOption = i + break + } } } - } - area, err := DefaultArea.Start(p.renderSelectMenu()) - if err != nil { - return "", fmt.Errorf("could not start area: %w", err) - } - - for p.result == "" { - keyInfo, err := keyboard.GetKey() + area, err := DefaultArea.Start(p.renderSelectMenu()) if err != nil { - return "", err + return true, fmt.Errorf("could not start area: %w", err) } - key := keyInfo.Code - switch key { - case keys.Up: - if p.selectedOption > 0 { - p.selectedOption-- + for p.result == "" { + key := keyInfo.Code + + switch key { + case keys.Up: + if p.selectedOption > 0 { + p.selectedOption-- + } + area.Update(p.renderSelectMenu()) + case keys.Down: + if p.selectedOption < len(p.Options)-1 { + p.selectedOption++ + } + area.Update(p.renderSelectMenu()) + case keys.CtrlC: + os.Exit(1) + case keys.Enter: + p.result = p.Options[p.selectedOption] } - area.Update(p.renderSelectMenu()) - case keys.Down: - if p.selectedOption < len(p.Options)-1 { - p.selectedOption++ - } - area.Update(p.renderSelectMenu()) - case keys.CtrlC: - os.Exit(1) - case keys.Enter: - p.result = p.Options[p.selectedOption] } - } - - err = keyboard.StopListener() + return true, nil + }) if err != nil { return "", fmt.Errorf("failed to start keyboard listener: %w", err) } From 98afa995c29dab6dc0bbd46f3c34006ec5b6a98f Mon Sep 17 00:00:00 2001 From: Marvin Wendt Date: Fri, 3 Jun 2022 17:03:22 +0200 Subject: [PATCH 04/25] added `InteractiveSelectPrinter` implementation --- interactive_confirm_printer.go | 70 +++++++++--------- interactive_select_printer.go | 127 +++++++++++++++++++++++---------- theme.go | 2 + 3 files changed, 124 insertions(+), 75 deletions(-) diff --git a/interactive_confirm_printer.go b/interactive_confirm_printer.go index 362ca3e59..e5200f9dc 100644 --- a/interactive_confirm_printer.go +++ b/interactive_confirm_printer.go @@ -80,52 +80,48 @@ func (p InteractiveConfirmPrinter) WithSuffixStyle(style *Style) *InteractiveCon // pterm.Println(result) func (p InteractiveConfirmPrinter) Show(text ...string) (bool, error) { var result bool - err := keyboard.Listen(func(keyInfo keys.Key) (stop bool, err error) { - if err != nil { - return false, fmt.Errorf("failed to start keyboard listener: %w", err) - } - if text == nil { - text = []string{"Please confirm"} - } + if text == nil || len(text) == 0 || text[0] == "" { + text = []string{"Please confirm"} + } - p.TextStyle.Print(text[0] + " " + p.getSuffix() + ": ") + p.TextStyle.Print(text[0] + " " + p.getSuffix() + ": ") - for { - key := keyInfo.Code - char := keyInfo.String() - if err != nil { - return false, fmt.Errorf("failed to get key: %w", err) - } + err := keyboard.Listen(func(keyInfo keys.Key) (stop bool, err error) { + key := keyInfo.Code + char := keyInfo.String() + if err != nil { + return false, fmt.Errorf("failed to get key: %w", err) + } - switch key { - case keys.RuneKey: - switch char { - case "y", "Y": - p.ConfirmStyle.Print(p.ConfirmText) - Println() - result = true - return true, nil - case "n", "N": - p.RejectStyle.Print(p.RejectText) - Println() - result = false - return true, nil - } - case keys.Enter: - if p.DefaultValue { - p.ConfirmStyle.Print(p.ConfirmText) - } else { - p.RejectStyle.Print(p.RejectText) - } + switch key { + case keys.RuneKey: + switch char { + case "y", "Y": + p.ConfirmStyle.Print(p.ConfirmText) Println() - result = p.DefaultValue + result = true return true, nil - case keys.CtrlC: - os.Exit(1) + case "n", "N": + p.RejectStyle.Print(p.RejectText) + Println() + result = false return true, nil } + case keys.Enter: + if p.DefaultValue { + p.ConfirmStyle.Print(p.ConfirmText) + } else { + p.RejectStyle.Print(p.RejectText) + } + Println() + result = p.DefaultValue + return true, nil + case keys.CtrlC: + os.Exit(1) + return true, nil } + return false, nil }) return result, err } diff --git a/interactive_select_printer.go b/interactive_select_printer.go index 0fed80bb7..202c48a21 100644 --- a/interactive_select_printer.go +++ b/interactive_select_printer.go @@ -4,22 +4,36 @@ import ( "fmt" "os" + "atomicgo.dev/cursor" "atomicgo.dev/keyboard" "atomicgo.dev/keyboard/keys" ) var ( // DefaultInteractiveSelect is the default InteractiveSelect printer. - DefaultInteractiveSelect = InteractiveSelectPrinter{} + DefaultInteractiveSelect = InteractiveSelectPrinter{ + TextStyle: &ThemeDefault.PrimaryStyle, + Options: []string{}, + OptionStyle: &ThemeDefault.DefaultText, + DefaultOption: "", + MaxHeight: 5, + Selector: ">", + SelectorStyle: &ThemeDefault.SecondaryStyle, + } ) type InteractiveSelectPrinter struct { + TextStyle *Style Options []string + OptionStyle *Style DefaultOption string MaxHeight int + Selector string + SelectorStyle *Style selectedOption int result string + text string } // WithOptions sets the options. @@ -34,65 +48,102 @@ func (p InteractiveSelectPrinter) WithDefaultOption(option string) *InteractiveS return &p } -func (p InteractiveSelectPrinter) Show(text ...string) (string, error) { - err := keyboard.Listen(func(keyInfo keys.Key) (stop bool, err error) { - if text == nil { - text = []string{""} - } +// WithMaxHeight sets the maximum height of the select menu. +func (p InteractiveSelectPrinter) WithMaxHeight(maxHeight int) *InteractiveSelectPrinter { + p.MaxHeight = maxHeight + return &p +} + +func (p *InteractiveSelectPrinter) Show(text ...string) (string, error) { + if text == nil || len(text) == 0 || text[0] == "" { + text = []string{"Please select an option"} + } + + p.text = p.TextStyle.Sprint(text[0]) - // Get index of default option - if p.DefaultOption != "" { - for i, option := range p.Options { - if option == p.DefaultOption { - p.selectedOption = i - break - } + if p.MaxHeight == 0 { + p.MaxHeight = DefaultInteractiveSelect.MaxHeight + } + + if len(p.Options) == 0 { + return "", fmt.Errorf("no options provided") + } + + // Get index of default option + if p.DefaultOption != "" { + for i, option := range p.Options { + if option == p.DefaultOption { + p.selectedOption = i + break } } + } - area, err := DefaultArea.Start(p.renderSelectMenu()) - if err != nil { - return true, fmt.Errorf("could not start area: %w", err) - } + area, err := DefaultArea.Start(p.renderSelectMenu()) + if err != nil { + return "", fmt.Errorf("could not start area: %w", err) + } - for p.result == "" { - key := keyInfo.Code - - switch key { - case keys.Up: - if p.selectedOption > 0 { - p.selectedOption-- - } - area.Update(p.renderSelectMenu()) - case keys.Down: - if p.selectedOption < len(p.Options)-1 { - p.selectedOption++ - } - area.Update(p.renderSelectMenu()) - case keys.CtrlC: - os.Exit(1) - case keys.Enter: - p.result = p.Options[p.selectedOption] + cursor.Hide() + err = keyboard.Listen(func(keyInfo keys.Key) (stop bool, err error) { + key := keyInfo.Code + + switch key { + case keys.Up: + if p.selectedOption > 0 { + p.selectedOption-- + } else { + p.selectedOption = len(p.Options) - 1 + } + area.Update(p.renderSelectMenu()) + case keys.Down: + if p.selectedOption < len(p.Options)-1 { + p.selectedOption++ + } else { + p.selectedOption = 0 } + area.Update(p.renderSelectMenu()) + case keys.CtrlC: + os.Exit(1) + case keys.Enter: + p.result = p.Options[p.selectedOption] + area.Update(p.renderFinishedMenu()) + return true, nil } - return true, nil + + return false, nil }) if err != nil { return "", fmt.Errorf("failed to start keyboard listener: %w", err) } + cursor.Show() + return p.result, nil } func (p InteractiveSelectPrinter) renderSelectMenu() string { var content string + content += Sprintf("%s:\n", p.text) for i, option := range p.Options { if i == p.selectedOption { - content += fmt.Sprintf("> %s\n", option) + content += Sprintf("%s %s\n", p.renderSelector(), option) } else { - content += fmt.Sprintf(" %s\n", option) + content += Sprintf(" %s\n", option) } } return content } + +func (p InteractiveSelectPrinter) renderFinishedMenu() string { + var content string + content += Sprintf("%s:\n", p.text) + content += Sprintf(" %s %s\n", p.renderSelector(), p.result) + + return content +} + +func (p InteractiveSelectPrinter) renderSelector() string { + return p.SelectorStyle.Sprint(p.Selector) +} diff --git a/theme.go b/theme.go index 379140b3a..b02ac3e28 100644 --- a/theme.go +++ b/theme.go @@ -4,6 +4,7 @@ var ( // ThemeDefault is the default theme used by PTerm. // If this variable is overwritten, the new value is used as default theme. ThemeDefault = Theme{ + DefaultText: Style{FgDefault, BgDefault}, PrimaryStyle: Style{FgLightCyan}, SecondaryStyle: Style{FgLightMagenta}, HighlightStyle: Style{Bold, FgYellow}, @@ -49,6 +50,7 @@ var ( // Theme contains every Style used in PTerm. You can create own themes for your application or use one // of the existing themes. type Theme struct { + DefaultText Style PrimaryStyle Style SecondaryStyle Style HighlightStyle Style From 511a91268af204b92d770145fa6d37e4039c3b1a Mon Sep 17 00:00:00 2001 From: MarvinJWendt Date: Sun, 12 Jun 2022 18:13:49 +0200 Subject: [PATCH 05/25] added fuzzy search to `InteractiveSelect` --- go.mod | 7 +-- go.sum | 23 ++++++++++ interactive_confirm_printer.go | 3 ++ interactive_confirm_printer_test.go | 49 +++++++++++++++++++++ interactive_select_printer.go | 67 +++++++++++++++++++++++------ interactive_select_printer_test.go | 35 +++++++++++++++ 6 files changed, 169 insertions(+), 15 deletions(-) create mode 100644 interactive_confirm_printer_test.go create mode 100644 interactive_select_printer_test.go diff --git a/go.mod b/go.mod index f5624bb62..46edd442b 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,11 @@ module github.com/pterm/pterm go 1.15 require ( - atomicgo.dev/cursor v0.1.0 - atomicgo.dev/keyboard v0.2.0 - github.com/MarvinJWendt/testza v0.3.5 + atomicgo.dev/cursor v0.1.1 + atomicgo.dev/keyboard v0.2.8 + github.com/MarvinJWendt/testza v0.4.2 github.com/gookit/color v1.5.0 + github.com/lithammer/fuzzysearch v1.1.5 // indirect github.com/mattn/go-runewidth v0.0.13 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 ) diff --git a/go.sum b/go.sum index 4b64c8653..2d0b27c10 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,23 @@ atomicgo.dev/cursor v0.1.0 h1:hsNUAMs7ioxwHLW03XDoO3+5heMfP8tJsiW14XIKlkw= atomicgo.dev/cursor v0.1.0/go.mod h1:Lr4ZJB3U7DfPPOkbH7/6TOtJ4vFGHlgj1nc+n900IpU= +atomicgo.dev/cursor v0.1.1 h1:0t9sxQomCTRh5ug+hAMCs59x/UmC9QL6Ci5uosINKD4= +atomicgo.dev/cursor v0.1.1/go.mod h1:Lr4ZJB3U7DfPPOkbH7/6TOtJ4vFGHlgj1nc+n900IpU= atomicgo.dev/keyboard v0.2.0 h1:sn0NUNl7l8PHJAsoWEvdGiMsOTSpOVSDlyMI+9tjf6o= atomicgo.dev/keyboard v0.2.0/go.mod h1:BRn8gWyv0+lWhrYvSv4HX1eK6VOk2euOAMV1cv0fOZ4= +atomicgo.dev/keyboard v0.2.1 h1:89rfqHqXg+jnXOWiIKmHXX/0izVHnJeMDqvU/G2DYmA= +atomicgo.dev/keyboard v0.2.1/go.mod h1:BRn8gWyv0+lWhrYvSv4HX1eK6VOk2euOAMV1cv0fOZ4= +atomicgo.dev/keyboard v0.2.2 h1:MqOs/zpRqZCT0u1O2MVr1TVmQIFW7NppfgkJBrydKwk= +atomicgo.dev/keyboard v0.2.2/go.mod h1:BRn8gWyv0+lWhrYvSv4HX1eK6VOk2euOAMV1cv0fOZ4= +atomicgo.dev/keyboard v0.2.3 h1:i0E/u3tWgKqhf7K/27WjusH9342Jd5yx2fxczFxRmrw= +atomicgo.dev/keyboard v0.2.3/go.mod h1:BRn8gWyv0+lWhrYvSv4HX1eK6VOk2euOAMV1cv0fOZ4= +atomicgo.dev/keyboard v0.2.4 h1:GI0G8/bSa19KMCubQiafmi6kRC4/WpF8P5yTnq52G+g= +atomicgo.dev/keyboard v0.2.4/go.mod h1:BRn8gWyv0+lWhrYvSv4HX1eK6VOk2euOAMV1cv0fOZ4= +atomicgo.dev/keyboard v0.2.5 h1:hUo7TpJjiXVf/ZUFy5IIPg4/YDPUuT8mFltgukIJFgE= +atomicgo.dev/keyboard v0.2.5/go.mod h1:BRn8gWyv0+lWhrYvSv4HX1eK6VOk2euOAMV1cv0fOZ4= +atomicgo.dev/keyboard v0.2.7 h1:hUsvbh7ZbaBnQsI8cOatTZJyUkBy2u6H1/+4dkDl70M= +atomicgo.dev/keyboard v0.2.7/go.mod h1:BRn8gWyv0+lWhrYvSv4HX1eK6VOk2euOAMV1cv0fOZ4= +atomicgo.dev/keyboard v0.2.8 h1:Di09BitwZgdTV1hPyX/b9Cqxi8HVuJQwWivnZUEqlj4= +atomicgo.dev/keyboard v0.2.8/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ= github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs= github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8= github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII= @@ -10,6 +26,8 @@ github.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzX github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/2oUqKc6bF2c= github.com/MarvinJWendt/testza v0.3.5 h1:g9krITRRlIsF1eO9sUKXtiTw670gZIIk6T08Keeo1nM= github.com/MarvinJWendt/testza v0.3.5/go.mod h1:ExbTpWmA1z2E9HSskvrNcwApoX4F9bID692s10nuHRY= +github.com/MarvinJWendt/testza v0.4.2 h1:Vbw9GkSB5erJI2BPnBL9SVGV9myE+XmUSFahBGUhW2Q= +github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE= github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= @@ -28,6 +46,8 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lithammer/fuzzysearch v1.1.5 h1:Ag7aKU08wp0R9QCfF4GoGST9HbmAIeLP7xwMrOBEp1c= +github.com/lithammer/fuzzysearch v1.1.5/go.mod h1:1R1LRNk7yKid1BaQkmuLQaHruxcC4HmAH30Dh61Ih1Q= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -61,6 +81,9 @@ golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/interactive_confirm_printer.go b/interactive_confirm_printer.go index e5200f9dc..3b88d5d53 100644 --- a/interactive_confirm_printer.go +++ b/interactive_confirm_printer.go @@ -10,6 +10,8 @@ import ( var ( // DefaultInteractiveConfirm is the default InteractiveConfirm printer. + // Pressing "y" will return true, "n" will return false. + // Pressing enter without typing "y" or "n" will return the configured default value (by default set to "no"). DefaultInteractiveConfirm = InteractiveConfirmPrinter{ DefaultValue: false, TextStyle: &ThemeDefault.PrimaryStyle, @@ -21,6 +23,7 @@ var ( } ) +// InteractiveConfirmPrinter is a printer for interactive confirm prompts. type InteractiveConfirmPrinter struct { DefaultValue bool TextStyle *Style diff --git a/interactive_confirm_printer_test.go b/interactive_confirm_printer_test.go new file mode 100644 index 000000000..74a111df9 --- /dev/null +++ b/interactive_confirm_printer_test.go @@ -0,0 +1,49 @@ +package pterm_test + +import ( + "testing" + + "atomicgo.dev/keyboard" + "atomicgo.dev/keyboard/keys" + "github.com/MarvinJWendt/testza" + "github.com/pterm/pterm" +) + +func TestInteractiveConfirmPrinter_Show_yes(t *testing.T) { + go func() { + keyboard.SimulateKeyPress('y') + }() + result, _ := pterm.DefaultInteractiveConfirm.Show() + testza.AssertTrue(t, result) +} + +func TestInteractiveConfirmPrinter_Show_no(t *testing.T) { + go func() { + keyboard.SimulateKeyPress('n') + }() + result, _ := pterm.DefaultInteractiveConfirm.Show() + testza.AssertFalse(t, result) +} + +func TestInteractiveConfirmPrinter_WithDefaultValue(t *testing.T) { + p := pterm.DefaultInteractiveConfirm.WithDefaultValue(true) + testza.AssertTrue(t, p.DefaultValue) +} + +func TestInteractiveConfirmPrinter_WithDefaultValue_false(t *testing.T) { + go func() { + keyboard.SimulateKeyPress(keys.Enter) + }() + p := pterm.DefaultInteractiveConfirm.WithDefaultValue(false) + result, _ := p.Show() + testza.AssertFalse(t, result) +} + +func TestInteractiveConfirmPrinter_WithDefaultValue_true(t *testing.T) { + go func() { + keyboard.SimulateKeyPress(keys.Enter) + }() + p := pterm.DefaultInteractiveConfirm.WithDefaultValue(true) + result, _ := p.Show() + testza.AssertTrue(t, result) +} diff --git a/interactive_select_printer.go b/interactive_select_printer.go index 202c48a21..5a520a173 100644 --- a/interactive_select_printer.go +++ b/interactive_select_printer.go @@ -3,10 +3,12 @@ package pterm import ( "fmt" "os" + "sort" "atomicgo.dev/cursor" "atomicgo.dev/keyboard" "atomicgo.dev/keyboard/keys" + "github.com/lithammer/fuzzysearch/fuzzy" ) var ( @@ -22,6 +24,7 @@ var ( } ) +// InteractiveSelectPrinter is a printer for interactive select menus. type InteractiveSelectPrinter struct { TextStyle *Style Options []string @@ -31,9 +34,11 @@ type InteractiveSelectPrinter struct { Selector string SelectorStyle *Style - selectedOption int - result string - text string + selectedOption int + result string + text string + fuzzySearchString string + fuzzySearchMatches []string } // WithOptions sets the options. @@ -55,11 +60,12 @@ func (p InteractiveSelectPrinter) WithMaxHeight(maxHeight int) *InteractiveSelec } func (p *InteractiveSelectPrinter) Show(text ...string) (string, error) { - if text == nil || len(text) == 0 || text[0] == "" { + if len(text) == 0 || text[0] == "" { text = []string{"Please select an option"} } p.text = p.TextStyle.Sprint(text[0]) + p.fuzzySearchMatches = p.Options if p.MaxHeight == 0 { p.MaxHeight = DefaultInteractiveSelect.MaxHeight @@ -80,24 +86,49 @@ func (p *InteractiveSelectPrinter) Show(text ...string) (string, error) { } area, err := DefaultArea.Start(p.renderSelectMenu()) + defer area.Stop() if err != nil { return "", fmt.Errorf("could not start area: %w", err) } cursor.Hide() + defer cursor.Show() err = keyboard.Listen(func(keyInfo keys.Key) (stop bool, err error) { key := keyInfo.Code switch key { + case keys.RuneKey: + // Fuzzy search for options + + // append to fuzzy search string + p.fuzzySearchString += keyInfo.String() + + p.selectedOption = 0 + area.Update(p.renderSelectMenu()) + case keys.Space: + p.fuzzySearchString += " " + p.selectedOption = 0 + area.Update(p.renderSelectMenu()) + case keys.Backspace: + // Remove last character from fuzzy search string + if len(p.fuzzySearchString) > 0 { + p.fuzzySearchString = p.fuzzySearchString[:len(p.fuzzySearchString)-1] + } + + if p.fuzzySearchString == "" { + p.fuzzySearchMatches = p.Options + } + + area.Update(p.renderSelectMenu()) case keys.Up: if p.selectedOption > 0 { p.selectedOption-- } else { - p.selectedOption = len(p.Options) - 1 + p.selectedOption = len(p.fuzzySearchMatches) - 1 } area.Update(p.renderSelectMenu()) case keys.Down: - if p.selectedOption < len(p.Options)-1 { + if p.selectedOption < len(p.fuzzySearchMatches)-1 { p.selectedOption++ } else { p.selectedOption = 0 @@ -106,7 +137,10 @@ func (p *InteractiveSelectPrinter) Show(text ...string) (string, error) { case keys.CtrlC: os.Exit(1) case keys.Enter: - p.result = p.Options[p.selectedOption] + if len(p.fuzzySearchMatches) == 0 { + return false, nil + } + p.result = p.fuzzySearchMatches[p.selectedOption] area.Update(p.renderFinishedMenu()) return true, nil } @@ -114,18 +148,27 @@ func (p *InteractiveSelectPrinter) Show(text ...string) (string, error) { return false, nil }) if err != nil { + fmt.Println(err) return "", fmt.Errorf("failed to start keyboard listener: %w", err) } - cursor.Show() - return p.result, nil } func (p InteractiveSelectPrinter) renderSelectMenu() string { var content string - content += Sprintf("%s:\n", p.text) - for i, option := range p.Options { + content += Sprintf("%s %s: %s\n", p.text, ThemeDefault.SecondaryStyle.Sprint("[type to search]"), p.fuzzySearchString) + + // find options that match fuzzy search string + rankedResults := fuzzy.RankFind(p.fuzzySearchString, p.fuzzySearchMatches) + // map rankedResults to fuzzySearchMatches + p.fuzzySearchMatches = []string{} + sort.Sort(rankedResults) + for _, result := range rankedResults { + p.fuzzySearchMatches = append(p.fuzzySearchMatches, result.Target) + } + + for i, option := range p.fuzzySearchMatches { if i == p.selectedOption { content += Sprintf("%s %s\n", p.renderSelector(), option) } else { @@ -138,7 +181,7 @@ func (p InteractiveSelectPrinter) renderSelectMenu() string { func (p InteractiveSelectPrinter) renderFinishedMenu() string { var content string - content += Sprintf("%s:\n", p.text) + content += Sprintf("%s: %s\n", p.text, p.fuzzySearchString) content += Sprintf(" %s %s\n", p.renderSelector(), p.result) return content diff --git a/interactive_select_printer_test.go b/interactive_select_printer_test.go new file mode 100644 index 000000000..53b7a2a4c --- /dev/null +++ b/interactive_select_printer_test.go @@ -0,0 +1,35 @@ +package pterm_test + +import ( + "testing" + + "atomicgo.dev/keyboard" + "atomicgo.dev/keyboard/keys" + "github.com/MarvinJWendt/testza" + "github.com/pterm/pterm" +) + +func TestInteractiveSelectPrinter_Show(t *testing.T) { + go func() { + keyboard.SimulateKeyPress(keys.Down) + keyboard.SimulateKeyPress(keys.Down) + keyboard.SimulateKeyPress(keys.Enter) + }() + result, _ := pterm.DefaultInteractiveSelect.WithOptions([]string{"a", "b", "c", "d", "e"}).WithDefaultOption("b").Show() + testza.AssertEqual(t, "d", result) +} + +func TestInteractiveSelectPrinter_WithDefaultOption(t *testing.T) { + p := pterm.DefaultInteractiveSelect.WithDefaultOption("default") + testza.AssertEqual(t, p.DefaultOption, "default") +} + +func TestInteractiveSelectPrinter_WithOptions(t *testing.T) { + p := pterm.DefaultInteractiveSelect.WithOptions([]string{"a", "b", "c"}) + testza.AssertEqual(t, p.Options, []string{"a", "b", "c"}) +} + +func TestInteractiveSelectPrinter_WithMaxHeight(t *testing.T) { + p := pterm.DefaultInteractiveSelect.WithMaxHeight(1337) + testza.AssertEqual(t, p.MaxHeight, 1337) +} From c851bb21c1c79aa5b39a1aac76dc7ecba4a11cd7 Mon Sep 17 00:00:00 2001 From: MarvinJWendt Date: Thu, 16 Jun 2022 00:00:15 +0200 Subject: [PATCH 06/25] added examples for interactive printers --- _examples/box/demo/main.go | 2 +- _examples/interactive_confirm/demo/ci.go | 19 +++++++++++++++++ _examples/interactive_confirm/demo/main.go | 18 ++++++++++++++++ _examples/interactive_select/demo/ci.go | 24 ++++++++++++++++++++++ _examples/interactive_select/demo/main.go | 10 +++++++++ interactive_confirm_printer.go | 2 ++ 6 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 _examples/interactive_confirm/demo/ci.go create mode 100644 _examples/interactive_confirm/demo/main.go create mode 100644 _examples/interactive_select/demo/ci.go create mode 100644 _examples/interactive_select/demo/main.go diff --git a/_examples/box/demo/main.go b/_examples/box/demo/main.go index f25a80e3a..52fc9e256 100644 --- a/_examples/box/demo/main.go +++ b/_examples/box/demo/main.go @@ -3,7 +3,7 @@ package main import "github.com/pterm/pterm" func main() { - pterm.Info.Println("This might not be rendered correctly on GitHub,\nbut it will work in a real terminal.\nThis is because GitHub does not use a monospaced font by default for SVGs.") + pterm.Info.Println("This might not be rendered correctly on GitHub,\nbut it will work in a real terminal.\nThis is because GitHub does not use a monospaced font by default for SVGs") panel1 := pterm.DefaultBox.Sprint("Lorem ipsum dolor sit amet,\nconsectetur adipiscing elit,\nsed do eiusmod tempor incididunt\nut labore et dolore\nmagna aliqua.") panel2 := pterm.DefaultBox.WithTitle("title").Sprint("Ut enim ad minim veniam,\nquis nostrud exercitation\nullamco laboris\nnisi ut aliquip\nex ea commodo\nconsequat.") diff --git a/_examples/interactive_confirm/demo/ci.go b/_examples/interactive_confirm/demo/ci.go new file mode 100644 index 000000000..2ba46081b --- /dev/null +++ b/_examples/interactive_confirm/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_confirm/demo/main.go b/_examples/interactive_confirm/demo/main.go new file mode 100644 index 000000000..9c27aa0ca --- /dev/null +++ b/_examples/interactive_confirm/demo/main.go @@ -0,0 +1,18 @@ +package main + +import ( + "github.com/pterm/pterm" +) + +func main() { + result, _ := pterm.DefaultInteractiveConfirm.Show() + pterm.Println() // Blank line + pterm.Info.Printfln("You answered: %s", boolToText(result)) +} + +func boolToText(b bool) string { + if b { + return pterm.Green("Yes") + } + return pterm.Red("No") +} diff --git a/_examples/interactive_select/demo/ci.go b/_examples/interactive_select/demo/ci.go new file mode 100644 index 000000000..f629cea7d --- /dev/null +++ b/_examples/interactive_select/demo/ci.go @@ -0,0 +1,24 @@ +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) + keyboard.SimulateKeyPress(keys.Down) + time.Sleep(time.Second) + keyboard.SimulateKeyPress(keys.Down) + time.Sleep(time.Second) + keyboard.SimulateKeyPress(keys.Enter) + }() + } +} diff --git a/_examples/interactive_select/demo/main.go b/_examples/interactive_select/demo/main.go new file mode 100644 index 000000000..cec96479f --- /dev/null +++ b/_examples/interactive_select/demo/main.go @@ -0,0 +1,10 @@ +package main + +import ( + "github.com/pterm/pterm" +) + +func main() { + selectedOption, _ := pterm.DefaultInteractiveSelect.WithOptions([]string{"Option 1", "Option 2", "Option 3", "Option 4"}).Show() + pterm.Info.Printfln("Selected option: %s", pterm.Green(selectedOption)) +} diff --git a/interactive_confirm_printer.go b/interactive_confirm_printer.go index 3b88d5d53..115c2b903 100644 --- a/interactive_confirm_printer.go +++ b/interactive_confirm_printer.go @@ -4,6 +4,7 @@ import ( "fmt" "os" + "atomicgo.dev/cursor" "atomicgo.dev/keyboard" "atomicgo.dev/keyboard/keys" ) @@ -126,6 +127,7 @@ func (p InteractiveConfirmPrinter) Show(text ...string) (bool, error) { } return false, nil }) + cursor.StartOfLine() return result, err } From 460130a3c35d3033098534c2f8c1d17524f834e8 Mon Sep 17 00:00:00 2001 From: MarvinJWendt Date: Thu, 16 Jun 2022 01:07:22 +0200 Subject: [PATCH 07/25] fixed fuzzy search --- interactive_select_printer.go | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/interactive_select_printer.go b/interactive_select_printer.go index 5a520a173..dd7cde3d0 100644 --- a/interactive_select_printer.go +++ b/interactive_select_printer.go @@ -59,13 +59,14 @@ func (p InteractiveSelectPrinter) WithMaxHeight(maxHeight int) *InteractiveSelec return &p } +// Show shows the interactive select menu and returns the selected entry. func (p *InteractiveSelectPrinter) Show(text ...string) (string, error) { - if len(text) == 0 || text[0] == "" { + if len(text) == 0 || Sprint(text[0]) == "" { text = []string{"Please select an option"} } p.text = p.TextStyle.Sprint(text[0]) - p.fuzzySearchMatches = p.Options + p.fuzzySearchMatches = append([]string{}, p.Options...) if p.MaxHeight == 0 { p.MaxHeight = DefaultInteractiveSelect.MaxHeight @@ -102,7 +103,6 @@ func (p *InteractiveSelectPrinter) Show(text ...string) (string, error) { // append to fuzzy search string p.fuzzySearchString += keyInfo.String() - p.selectedOption = 0 area.Update(p.renderSelectMenu()) case keys.Space: @@ -112,11 +112,12 @@ func (p *InteractiveSelectPrinter) Show(text ...string) (string, error) { case keys.Backspace: // Remove last character from fuzzy search string if len(p.fuzzySearchString) > 0 { - p.fuzzySearchString = p.fuzzySearchString[:len(p.fuzzySearchString)-1] + // Handle UTF-8 characters + p.fuzzySearchString = string([]rune(p.fuzzySearchString)[:len([]rune(p.fuzzySearchString))-1]) } if p.fuzzySearchString == "" { - p.fuzzySearchMatches = p.Options + p.fuzzySearchMatches = append([]string{}, p.Options...) } area.Update(p.renderSelectMenu()) @@ -140,7 +141,6 @@ func (p *InteractiveSelectPrinter) Show(text ...string) (string, error) { if len(p.fuzzySearchMatches) == 0 { return false, nil } - p.result = p.fuzzySearchMatches[p.selectedOption] area.Update(p.renderFinishedMenu()) return true, nil } @@ -155,19 +155,25 @@ func (p *InteractiveSelectPrinter) Show(text ...string) (string, error) { return p.result, nil } -func (p InteractiveSelectPrinter) renderSelectMenu() string { +func (p *InteractiveSelectPrinter) renderSelectMenu() string { var content string content += Sprintf("%s %s: %s\n", p.text, ThemeDefault.SecondaryStyle.Sprint("[type to search]"), p.fuzzySearchString) // find options that match fuzzy search string - rankedResults := fuzzy.RankFind(p.fuzzySearchString, p.fuzzySearchMatches) + rankedResults := fuzzy.RankFindFold(p.fuzzySearchString, p.fuzzySearchMatches) // map rankedResults to fuzzySearchMatches p.fuzzySearchMatches = []string{} - sort.Sort(rankedResults) + if len(rankedResults) != len(p.Options) { + sort.Sort(rankedResults) + } for _, result := range rankedResults { p.fuzzySearchMatches = append(p.fuzzySearchMatches, result.Target) } + if len(p.fuzzySearchMatches) != 0 { + p.result = p.fuzzySearchMatches[p.selectedOption] + } + for i, option := range p.fuzzySearchMatches { if i == p.selectedOption { content += Sprintf("%s %s\n", p.renderSelector(), option) From 21ed62747ddb5c9dea2600ea9ecf7bcc366f1326 Mon Sep 17 00:00:00 2001 From: MarvinJWendt Date: Thu, 16 Jun 2022 02:53:24 +0200 Subject: [PATCH 08/25] implemented select menu scrolling --- interactive_select_printer.go | 69 +++++++++++++++++++++++++++++++---- 1 file changed, 61 insertions(+), 8 deletions(-) diff --git a/interactive_select_printer.go b/interactive_select_printer.go index dd7cde3d0..2554e37f8 100644 --- a/interactive_select_printer.go +++ b/interactive_select_printer.go @@ -34,11 +34,14 @@ type InteractiveSelectPrinter struct { Selector string SelectorStyle *Style - selectedOption int - result string - text string - fuzzySearchString string - fuzzySearchMatches []string + selectedOption int + result string + text string + fuzzySearchString string + fuzzySearchMatches []string + displayedOptions []string + displayedOptionsStart int + displayedOptionsEnd int } // WithOptions sets the options. @@ -76,11 +79,23 @@ func (p *InteractiveSelectPrinter) Show(text ...string) (string, error) { return "", fmt.Errorf("no options provided") } + p.displayedOptions = append([]string{}, p.fuzzySearchMatches[:p.MaxHeight]...) + p.displayedOptionsStart = 0 + p.displayedOptionsEnd = p.MaxHeight + // Get index of default option if p.DefaultOption != "" { for i, option := range p.Options { if option == p.DefaultOption { p.selectedOption = i + if i > 0 { + p.displayedOptionsStart = i - 1 + p.displayedOptionsEnd = i - 1 + p.MaxHeight + } else { + p.displayedOptionsStart = 0 + p.displayedOptionsEnd = p.MaxHeight + } + p.displayedOptions = p.Options[p.displayedOptionsStart:p.displayedOptionsEnd] break } } @@ -92,6 +107,8 @@ func (p *InteractiveSelectPrinter) Show(text ...string) (string, error) { return "", fmt.Errorf("could not start area: %w", err) } + area.Update(p.renderSelectMenu()) + cursor.Hide() defer cursor.Show() err = keyboard.Listen(func(keyInfo keys.Key) (stop bool, err error) { @@ -100,10 +117,12 @@ func (p *InteractiveSelectPrinter) Show(text ...string) (string, error) { switch key { case keys.RuneKey: // Fuzzy search for options - // append to fuzzy search string p.fuzzySearchString += keyInfo.String() p.selectedOption = 0 + p.displayedOptionsStart = 0 + p.displayedOptionsEnd = p.MaxHeight + p.displayedOptions = append([]string{}, p.fuzzySearchMatches[:p.MaxHeight]...) area.Update(p.renderSelectMenu()) case keys.Space: p.fuzzySearchString += " " @@ -124,16 +143,39 @@ func (p *InteractiveSelectPrinter) Show(text ...string) (string, error) { case keys.Up: if p.selectedOption > 0 { p.selectedOption-- + if p.selectedOption < p.displayedOptionsStart { + p.displayedOptionsStart-- + p.displayedOptionsEnd-- + if p.displayedOptionsStart < 0 { + p.displayedOptionsStart = 0 + p.displayedOptionsEnd = p.MaxHeight + } + p.displayedOptions = p.fuzzySearchMatches[p.displayedOptionsStart:p.displayedOptionsEnd] + } } else { p.selectedOption = len(p.fuzzySearchMatches) - 1 + p.displayedOptionsStart = len(p.fuzzySearchMatches) - p.MaxHeight + p.displayedOptionsEnd = len(p.fuzzySearchMatches) + p.displayedOptions = p.fuzzySearchMatches[p.displayedOptionsStart:p.displayedOptionsEnd] } + area.Update(p.renderSelectMenu()) case keys.Down: + p.displayedOptions = p.fuzzySearchMatches[:p.MaxHeight] if p.selectedOption < len(p.fuzzySearchMatches)-1 { p.selectedOption++ + if p.selectedOption >= p.displayedOptionsEnd { + p.displayedOptionsStart++ + p.displayedOptionsEnd++ + p.displayedOptions = p.fuzzySearchMatches[p.displayedOptionsStart:p.displayedOptionsEnd] + } } else { p.selectedOption = 0 + p.displayedOptionsStart = 0 + p.displayedOptionsEnd = p.MaxHeight + p.displayedOptions = p.fuzzySearchMatches[p.displayedOptionsStart:p.displayedOptionsEnd] } + area.Update(p.renderSelectMenu()) case keys.CtrlC: os.Exit(1) @@ -160,7 +202,7 @@ func (p *InteractiveSelectPrinter) renderSelectMenu() string { content += Sprintf("%s %s: %s\n", p.text, ThemeDefault.SecondaryStyle.Sprint("[type to search]"), p.fuzzySearchString) // find options that match fuzzy search string - rankedResults := fuzzy.RankFindFold(p.fuzzySearchString, p.fuzzySearchMatches) + rankedResults := fuzzy.RankFindFold(p.fuzzySearchString, p.Options) // map rankedResults to fuzzySearchMatches p.fuzzySearchMatches = []string{} if len(rankedResults) != len(p.Options) { @@ -174,7 +216,18 @@ func (p *InteractiveSelectPrinter) renderSelectMenu() string { p.result = p.fuzzySearchMatches[p.selectedOption] } - for i, option := range p.fuzzySearchMatches { + indexMapper := make([]string, len(p.fuzzySearchMatches)) + for i := 0; i < len(p.fuzzySearchMatches); i++ { + // if in displayed options range + if i >= p.displayedOptionsStart && i < p.displayedOptionsEnd { + indexMapper[i] = p.fuzzySearchMatches[i] + } + } + + for i, option := range indexMapper { + if option == "" { + continue + } if i == p.selectedOption { content += Sprintf("%s %s\n", p.renderSelector(), option) } else { From 5703d7bae292b4d8f5dc08eeda74921cf751d5d3 Mon Sep 17 00:00:00 2001 From: MarvinJWendt Date: Thu, 16 Jun 2022 21:17:00 +0200 Subject: [PATCH 09/25] fixed select printer --- interactive_select_printer.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/interactive_select_printer.go b/interactive_select_printer.go index 2554e37f8..48877e361 100644 --- a/interactive_select_printer.go +++ b/interactive_select_printer.go @@ -114,6 +114,11 @@ func (p *InteractiveSelectPrinter) Show(text ...string) (string, error) { err = keyboard.Listen(func(keyInfo keys.Key) (stop bool, err error) { key := keyInfo.Code + maxHeight := p.MaxHeight + if maxHeight > len(p.fuzzySearchMatches) { + maxHeight = len(p.fuzzySearchMatches) + } + switch key { case keys.RuneKey: // Fuzzy search for options @@ -122,7 +127,7 @@ func (p *InteractiveSelectPrinter) Show(text ...string) (string, error) { p.selectedOption = 0 p.displayedOptionsStart = 0 p.displayedOptionsEnd = p.MaxHeight - p.displayedOptions = append([]string{}, p.fuzzySearchMatches[:p.MaxHeight]...) + p.displayedOptions = append([]string{}, p.fuzzySearchMatches[:maxHeight]...) area.Update(p.renderSelectMenu()) case keys.Space: p.fuzzySearchString += " " @@ -141,6 +146,9 @@ func (p *InteractiveSelectPrinter) Show(text ...string) (string, error) { area.Update(p.renderSelectMenu()) case keys.Up: + if len(p.fuzzySearchMatches) == 0 { + return false, nil + } if p.selectedOption > 0 { p.selectedOption-- if p.selectedOption < p.displayedOptionsStart { @@ -161,6 +169,9 @@ func (p *InteractiveSelectPrinter) Show(text ...string) (string, error) { area.Update(p.renderSelectMenu()) case keys.Down: + if len(p.fuzzySearchMatches) == 0 { + return false, nil + } p.displayedOptions = p.fuzzySearchMatches[:p.MaxHeight] if p.selectedOption < len(p.fuzzySearchMatches)-1 { p.selectedOption++ From 62bd0e59d63e28f76f529e0c22b5dcff84e0a141 Mon Sep 17 00:00:00 2001 From: MarvinJWendt Date: Thu, 16 Jun 2022 21:23:51 +0200 Subject: [PATCH 10/25] fixed max height bug --- interactive_select_printer.go | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/interactive_select_printer.go b/interactive_select_printer.go index 48877e361..769c13ef6 100644 --- a/interactive_select_printer.go +++ b/interactive_select_printer.go @@ -75,13 +75,18 @@ func (p *InteractiveSelectPrinter) Show(text ...string) (string, error) { p.MaxHeight = DefaultInteractiveSelect.MaxHeight } + maxHeight := p.MaxHeight + if maxHeight > len(p.fuzzySearchMatches) { + maxHeight = len(p.fuzzySearchMatches) + } + if len(p.Options) == 0 { return "", fmt.Errorf("no options provided") } - p.displayedOptions = append([]string{}, p.fuzzySearchMatches[:p.MaxHeight]...) + p.displayedOptions = append([]string{}, p.fuzzySearchMatches[:maxHeight]...) p.displayedOptionsStart = 0 - p.displayedOptionsEnd = p.MaxHeight + p.displayedOptionsEnd = maxHeight // Get index of default option if p.DefaultOption != "" { @@ -90,10 +95,10 @@ func (p *InteractiveSelectPrinter) Show(text ...string) (string, error) { p.selectedOption = i if i > 0 { p.displayedOptionsStart = i - 1 - p.displayedOptionsEnd = i - 1 + p.MaxHeight + p.displayedOptionsEnd = i - 1 + maxHeight } else { p.displayedOptionsStart = 0 - p.displayedOptionsEnd = p.MaxHeight + p.displayedOptionsEnd = maxHeight } p.displayedOptions = p.Options[p.displayedOptionsStart:p.displayedOptionsEnd] break @@ -114,7 +119,6 @@ func (p *InteractiveSelectPrinter) Show(text ...string) (string, error) { err = keyboard.Listen(func(keyInfo keys.Key) (stop bool, err error) { key := keyInfo.Code - maxHeight := p.MaxHeight if maxHeight > len(p.fuzzySearchMatches) { maxHeight = len(p.fuzzySearchMatches) } @@ -126,7 +130,7 @@ func (p *InteractiveSelectPrinter) Show(text ...string) (string, error) { p.fuzzySearchString += keyInfo.String() p.selectedOption = 0 p.displayedOptionsStart = 0 - p.displayedOptionsEnd = p.MaxHeight + p.displayedOptionsEnd = maxHeight p.displayedOptions = append([]string{}, p.fuzzySearchMatches[:maxHeight]...) area.Update(p.renderSelectMenu()) case keys.Space: @@ -156,13 +160,13 @@ func (p *InteractiveSelectPrinter) Show(text ...string) (string, error) { p.displayedOptionsEnd-- if p.displayedOptionsStart < 0 { p.displayedOptionsStart = 0 - p.displayedOptionsEnd = p.MaxHeight + p.displayedOptionsEnd = maxHeight } p.displayedOptions = p.fuzzySearchMatches[p.displayedOptionsStart:p.displayedOptionsEnd] } } else { p.selectedOption = len(p.fuzzySearchMatches) - 1 - p.displayedOptionsStart = len(p.fuzzySearchMatches) - p.MaxHeight + p.displayedOptionsStart = len(p.fuzzySearchMatches) - maxHeight p.displayedOptionsEnd = len(p.fuzzySearchMatches) p.displayedOptions = p.fuzzySearchMatches[p.displayedOptionsStart:p.displayedOptionsEnd] } @@ -172,7 +176,7 @@ func (p *InteractiveSelectPrinter) Show(text ...string) (string, error) { if len(p.fuzzySearchMatches) == 0 { return false, nil } - p.displayedOptions = p.fuzzySearchMatches[:p.MaxHeight] + p.displayedOptions = p.fuzzySearchMatches[:maxHeight] if p.selectedOption < len(p.fuzzySearchMatches)-1 { p.selectedOption++ if p.selectedOption >= p.displayedOptionsEnd { @@ -183,7 +187,7 @@ func (p *InteractiveSelectPrinter) Show(text ...string) (string, error) { } else { p.selectedOption = 0 p.displayedOptionsStart = 0 - p.displayedOptionsEnd = p.MaxHeight + p.displayedOptionsEnd = maxHeight p.displayedOptions = p.fuzzySearchMatches[p.displayedOptionsStart:p.displayedOptionsEnd] } From 8725f04e165cdb47709d9093335a252825942a93 Mon Sep 17 00:00:00 2001 From: MarvinJWendt Date: Thu, 16 Jun 2022 21:25:41 +0200 Subject: [PATCH 11/25] fixed max height bug --- interactive_select_printer.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/interactive_select_printer.go b/interactive_select_printer.go index 769c13ef6..99facf048 100644 --- a/interactive_select_printer.go +++ b/interactive_select_printer.go @@ -121,6 +121,8 @@ func (p *InteractiveSelectPrinter) Show(text ...string) (string, error) { if maxHeight > len(p.fuzzySearchMatches) { maxHeight = len(p.fuzzySearchMatches) + } else { + maxHeight = p.MaxHeight } switch key { From 50ee39097bbf9d4c9b63ab1d3c08b3a37bf908e6 Mon Sep 17 00:00:00 2001 From: MarvinJWendt Date: Thu, 16 Jun 2022 21:57:30 +0200 Subject: [PATCH 12/25] fixed max height bug --- interactive_select_printer.go | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/interactive_select_printer.go b/interactive_select_printer.go index 99facf048..ad2d67023 100644 --- a/interactive_select_printer.go +++ b/interactive_select_printer.go @@ -119,7 +119,7 @@ func (p *InteractiveSelectPrinter) Show(text ...string) (string, error) { err = keyboard.Listen(func(keyInfo keys.Key) (stop bool, err error) { key := keyInfo.Code - if maxHeight > len(p.fuzzySearchMatches) { + if p.MaxHeight > len(p.fuzzySearchMatches) { maxHeight = len(p.fuzzySearchMatches) } else { maxHeight = p.MaxHeight @@ -150,6 +150,19 @@ func (p *InteractiveSelectPrinter) Show(text ...string) (string, error) { p.fuzzySearchMatches = append([]string{}, p.Options...) } + p.renderSelectMenu() + + if len(p.fuzzySearchMatches) > p.MaxHeight { + maxHeight = p.MaxHeight + } else { + maxHeight = len(p.fuzzySearchMatches) + } + + p.selectedOption = 0 + p.displayedOptionsStart = 0 + p.displayedOptionsEnd = maxHeight + p.displayedOptions = append([]string{}, p.fuzzySearchMatches[p.displayedOptionsStart:p.displayedOptionsEnd]...) + area.Update(p.renderSelectMenu()) case keys.Up: if len(p.fuzzySearchMatches) == 0 { @@ -164,13 +177,13 @@ func (p *InteractiveSelectPrinter) Show(text ...string) (string, error) { p.displayedOptionsStart = 0 p.displayedOptionsEnd = maxHeight } - p.displayedOptions = p.fuzzySearchMatches[p.displayedOptionsStart:p.displayedOptionsEnd] + p.displayedOptions = append([]string{}, p.fuzzySearchMatches[p.displayedOptionsStart:p.displayedOptionsEnd]...) } } else { p.selectedOption = len(p.fuzzySearchMatches) - 1 p.displayedOptionsStart = len(p.fuzzySearchMatches) - maxHeight p.displayedOptionsEnd = len(p.fuzzySearchMatches) - p.displayedOptions = p.fuzzySearchMatches[p.displayedOptionsStart:p.displayedOptionsEnd] + p.displayedOptions = append([]string{}, p.fuzzySearchMatches[p.displayedOptionsStart:p.displayedOptionsEnd]...) } area.Update(p.renderSelectMenu()) @@ -184,13 +197,13 @@ func (p *InteractiveSelectPrinter) Show(text ...string) (string, error) { if p.selectedOption >= p.displayedOptionsEnd { p.displayedOptionsStart++ p.displayedOptionsEnd++ - p.displayedOptions = p.fuzzySearchMatches[p.displayedOptionsStart:p.displayedOptionsEnd] + p.displayedOptions = append([]string{}, p.fuzzySearchMatches[p.displayedOptionsStart:p.displayedOptionsEnd]...) } } else { p.selectedOption = 0 p.displayedOptionsStart = 0 p.displayedOptionsEnd = maxHeight - p.displayedOptions = p.fuzzySearchMatches[p.displayedOptionsStart:p.displayedOptionsEnd] + p.displayedOptions = append([]string{}, p.fuzzySearchMatches[p.displayedOptionsStart:p.displayedOptionsEnd]...) } area.Update(p.renderSelectMenu()) From 9623eb6c8e2cc1b6925a91e198091fd262d66521 Mon Sep 17 00:00:00 2001 From: floaust Date: Fri, 17 Jun 2022 05:10:29 +0200 Subject: [PATCH 13/25] feat(input): added text input printer --- _examples/interactive_textinput/demo/ci.go | 35 +++ _examples/interactive_textinput/demo/main.go | 11 + .../interactive_textinput/multi-line/ci.go | 41 ++++ .../interactive_textinput/multi-line/main.go | 11 + go.mod | 2 +- go.sum | 18 -- interactive_textinput_printer.go | 208 ++++++++++++++++++ 7 files changed, 307 insertions(+), 19 deletions(-) create mode 100644 _examples/interactive_textinput/demo/ci.go create mode 100644 _examples/interactive_textinput/demo/main.go create mode 100644 _examples/interactive_textinput/multi-line/ci.go create mode 100644 _examples/interactive_textinput/multi-line/main.go create mode 100644 interactive_textinput_printer.go diff --git a/_examples/interactive_textinput/demo/ci.go b/_examples/interactive_textinput/demo/ci.go new file mode 100644 index 000000000..9c99622b1 --- /dev/null +++ b/_examples/interactive_textinput/demo/ci.go @@ -0,0 +1,35 @@ +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 []rune(input) { + keyboard.SimulateKeyPress(r) + time.Sleep(time.Millisecond * 250) + } + + for i := 0; i < 7; i++ { + keyboard.SimulateKeyPress(keys.Left) + time.Sleep(time.Millisecond * 150) + } + + keyboard.SimulateKeyPress(keys.Backspace) + time.Sleep(time.Millisecond * 500) + keyboard.SimulateKeyPress(',') + time.Sleep(time.Millisecond * 500) + keyboard.SimulateKeyPress(keys.Enter) + }() + } +} diff --git a/_examples/interactive_textinput/demo/main.go b/_examples/interactive_textinput/demo/main.go new file mode 100644 index 000000000..ac00eaf1c --- /dev/null +++ b/_examples/interactive_textinput/demo/main.go @@ -0,0 +1,11 @@ +package main + +import ( + "github.com/pterm/pterm" +) + +func main() { + result, _ := pterm.DefaultInteractiveTextInput.WithMultiLine(false).Show() + pterm.Println() // Blank line + pterm.Info.Printfln("You answered: %s", result) +} diff --git a/_examples/interactive_textinput/multi-line/ci.go b/_examples/interactive_textinput/multi-line/ci.go new file mode 100644 index 000000000..87cbaa44f --- /dev/null +++ b/_examples/interactive_textinput/multi-line/ci.go @@ -0,0 +1,41 @@ +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 := "11111112222222\n" + for _, r := range []rune(input) { + keyboard.SimulateKeyPress(r) + time.Sleep(time.Millisecond * 250) + } + + for i := 0; i < 7; i++ { + keyboard.SimulateKeyPress(keys.Left) + time.Sleep(time.Millisecond * 150) + } + + keyboard.SimulateKeyPress(keys.Backspace) + time.Sleep(time.Millisecond * 500) + keyboard.SimulateKeyPress(keys.Enter) + time.Sleep(time.Millisecond * 500) + input = "33333333\n4\n5555555" + for _, r := range []rune(input) { + keyboard.SimulateKeyPress(r) + time.Sleep(time.Millisecond * 250) + } + + keyboard.SimulateKeyPress(keys.Tab) + }() + } +} diff --git a/_examples/interactive_textinput/multi-line/main.go b/_examples/interactive_textinput/multi-line/main.go new file mode 100644 index 000000000..361192e6f --- /dev/null +++ b/_examples/interactive_textinput/multi-line/main.go @@ -0,0 +1,11 @@ +package main + +import ( + "github.com/pterm/pterm" +) + +func main() { + result, _ := pterm.DefaultInteractiveTextInput.WithMultiLine().Show() // Text input with multi line enabled + pterm.Println() // Blank line + pterm.Info.Printfln("You answered: %s", result) +} diff --git a/go.mod b/go.mod index 46edd442b..6311b3ee8 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( atomicgo.dev/keyboard v0.2.8 github.com/MarvinJWendt/testza v0.4.2 github.com/gookit/color v1.5.0 - github.com/lithammer/fuzzysearch v1.1.5 // indirect + github.com/lithammer/fuzzysearch v1.1.5 github.com/mattn/go-runewidth v0.0.13 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 ) diff --git a/go.sum b/go.sum index 2d0b27c10..38826b3b4 100644 --- a/go.sum +++ b/go.sum @@ -1,21 +1,5 @@ -atomicgo.dev/cursor v0.1.0 h1:hsNUAMs7ioxwHLW03XDoO3+5heMfP8tJsiW14XIKlkw= -atomicgo.dev/cursor v0.1.0/go.mod h1:Lr4ZJB3U7DfPPOkbH7/6TOtJ4vFGHlgj1nc+n900IpU= atomicgo.dev/cursor v0.1.1 h1:0t9sxQomCTRh5ug+hAMCs59x/UmC9QL6Ci5uosINKD4= atomicgo.dev/cursor v0.1.1/go.mod h1:Lr4ZJB3U7DfPPOkbH7/6TOtJ4vFGHlgj1nc+n900IpU= -atomicgo.dev/keyboard v0.2.0 h1:sn0NUNl7l8PHJAsoWEvdGiMsOTSpOVSDlyMI+9tjf6o= -atomicgo.dev/keyboard v0.2.0/go.mod h1:BRn8gWyv0+lWhrYvSv4HX1eK6VOk2euOAMV1cv0fOZ4= -atomicgo.dev/keyboard v0.2.1 h1:89rfqHqXg+jnXOWiIKmHXX/0izVHnJeMDqvU/G2DYmA= -atomicgo.dev/keyboard v0.2.1/go.mod h1:BRn8gWyv0+lWhrYvSv4HX1eK6VOk2euOAMV1cv0fOZ4= -atomicgo.dev/keyboard v0.2.2 h1:MqOs/zpRqZCT0u1O2MVr1TVmQIFW7NppfgkJBrydKwk= -atomicgo.dev/keyboard v0.2.2/go.mod h1:BRn8gWyv0+lWhrYvSv4HX1eK6VOk2euOAMV1cv0fOZ4= -atomicgo.dev/keyboard v0.2.3 h1:i0E/u3tWgKqhf7K/27WjusH9342Jd5yx2fxczFxRmrw= -atomicgo.dev/keyboard v0.2.3/go.mod h1:BRn8gWyv0+lWhrYvSv4HX1eK6VOk2euOAMV1cv0fOZ4= -atomicgo.dev/keyboard v0.2.4 h1:GI0G8/bSa19KMCubQiafmi6kRC4/WpF8P5yTnq52G+g= -atomicgo.dev/keyboard v0.2.4/go.mod h1:BRn8gWyv0+lWhrYvSv4HX1eK6VOk2euOAMV1cv0fOZ4= -atomicgo.dev/keyboard v0.2.5 h1:hUo7TpJjiXVf/ZUFy5IIPg4/YDPUuT8mFltgukIJFgE= -atomicgo.dev/keyboard v0.2.5/go.mod h1:BRn8gWyv0+lWhrYvSv4HX1eK6VOk2euOAMV1cv0fOZ4= -atomicgo.dev/keyboard v0.2.7 h1:hUsvbh7ZbaBnQsI8cOatTZJyUkBy2u6H1/+4dkDl70M= -atomicgo.dev/keyboard v0.2.7/go.mod h1:BRn8gWyv0+lWhrYvSv4HX1eK6VOk2euOAMV1cv0fOZ4= atomicgo.dev/keyboard v0.2.8 h1:Di09BitwZgdTV1hPyX/b9Cqxi8HVuJQwWivnZUEqlj4= atomicgo.dev/keyboard v0.2.8/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ= github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs= @@ -24,8 +8,6 @@ github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBE github.com/MarvinJWendt/testza v0.2.10/go.mod h1:pd+VWsoGUiFtq+hRKSU1Bktnn+DMCSrDrXDpX2bG66k= github.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzXjB69adAhzZkI= github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/2oUqKc6bF2c= -github.com/MarvinJWendt/testza v0.3.5 h1:g9krITRRlIsF1eO9sUKXtiTw670gZIIk6T08Keeo1nM= -github.com/MarvinJWendt/testza v0.3.5/go.mod h1:ExbTpWmA1z2E9HSskvrNcwApoX4F9bID692s10nuHRY= github.com/MarvinJWendt/testza v0.4.2 h1:Vbw9GkSB5erJI2BPnBL9SVGV9myE+XmUSFahBGUhW2Q= github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE= github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= diff --git a/interactive_textinput_printer.go b/interactive_textinput_printer.go new file mode 100644 index 000000000..b177e9769 --- /dev/null +++ b/interactive_textinput_printer.go @@ -0,0 +1,208 @@ +package pterm + +import ( + "os" + "strings" + + "atomicgo.dev/cursor" + "atomicgo.dev/keyboard" + "atomicgo.dev/keyboard/keys" + + "github.com/pterm/pterm/internal" +) + +var ( + // DefaultInteractiveTextInput is the default InteractiveTextInput printer. + DefaultInteractiveTextInput = InteractiveTextInputPrinter{ + TextStyle: &ThemeDefault.PrimaryStyle, + } +) + +// InteractiveTextInputPrinter is a printer for interactive select menus. +type InteractiveTextInputPrinter struct { + TextStyle *Style + MultiLine bool + + input []string + cursorXPos int + cursorYPos int + text string +} + +// WithTextStyle sets the text style. +func (p *InteractiveTextInputPrinter) WithTextStyle(style *Style) *InteractiveTextInputPrinter { + p.TextStyle = style + return p +} + +// WithMultiLine sets the multi line flag. +func (p *InteractiveTextInputPrinter) WithMultiLine(multiLine ...bool) *InteractiveTextInputPrinter { + p.MultiLine = internal.WithBoolean(multiLine) + return p +} + +// Show shows the interactive select menu and returns the selected entry. +func (p *InteractiveTextInputPrinter) Show(text ...string) (string, error) { + var areaText string + + if len(text) == 0 || Sprint(text[0]) == "" { + text = []string{"Input text"} + } + + if p.MultiLine { + areaText = p.TextStyle.Sprint(text[0] + ": \n") + } else { + areaText = p.TextStyle.Sprint(text[0] + ": ") + } + p.text = areaText + area, err := DefaultArea.Start(areaText) + defer area.Stop() + if err != nil { + return "", err + } + + cursor.Up(1) + cursor.StartOfLine() + if !p.MultiLine { + cursor.Right(len(RemoveColorFromString(text[0])) + 2) + } + + err = keyboard.Listen(func(key keys.Key) (stop bool, err error) { + if !p.MultiLine { + p.cursorYPos = 0 + } + if len(p.input) == 0 { + p.input = append(p.input, "") + } + + switch key.Code { + case keys.Tab: + if p.MultiLine { + return true, nil + } + case keys.Enter: + if p.MultiLine { + if key.AltPressed { + p.cursorXPos = 0 + } + appendAfterY := append([]string{}, p.input[p.cursorYPos+1:]...) + appendAfterX := string(append([]rune{}, []rune(p.input[p.cursorYPos])[len([]rune(p.input[p.cursorYPos]))+p.cursorXPos:]...)) + p.input[p.cursorYPos] = string(append([]rune{}, []rune(p.input[p.cursorYPos])[:len([]rune(p.input[p.cursorYPos]))+p.cursorXPos]...)) + p.input = append(p.input[:p.cursorYPos+1], appendAfterX) + p.input = append(p.input, appendAfterY...) + p.cursorYPos++ + p.cursorXPos = -internal.GetStringMaxWidth(p.input[p.cursorYPos]) + cursor.Down(1) + cursor.StartOfLine() + } else { + return true, nil + } + case keys.RuneKey: + p.input[p.cursorYPos] = string(append([]rune(p.input[p.cursorYPos])[:len([]rune(p.input[p.cursorYPos]))+p.cursorXPos], append([]rune(key.String()), []rune(p.input[p.cursorYPos])[len([]rune(p.input[p.cursorYPos]))+p.cursorXPos:]...)...)) + case keys.Space: + p.input[p.cursorYPos] = string(append([]rune(p.input[p.cursorYPos])[:len([]rune(p.input[p.cursorYPos]))+p.cursorXPos], append([]rune(" "), []rune(p.input[p.cursorYPos])[len([]rune(p.input[p.cursorYPos]))+p.cursorXPos:]...)...)) + case keys.Backspace: + if len([]rune(p.input[p.cursorYPos]))+p.cursorXPos > 0 { + p.input[p.cursorYPos] = string(append([]rune(p.input[p.cursorYPos])[:len([]rune(p.input[p.cursorYPos]))-1+p.cursorXPos], []rune(p.input[p.cursorYPos])[len([]rune(p.input[p.cursorYPos]))+p.cursorXPos:]...)) + } else if p.cursorYPos > 0 { + p.input[p.cursorYPos-1] += p.input[p.cursorYPos] + appendAfterY := append([]string{}, p.input[p.cursorYPos+1:]...) + p.input = append(p.input[:p.cursorYPos], appendAfterY...) + p.cursorXPos = 0 + p.cursorYPos-- + } + case keys.Delete: + if len([]rune(p.input[p.cursorYPos]))+p.cursorXPos < len([]rune(p.input[p.cursorYPos])) { + p.input[p.cursorYPos] = string(append([]rune(p.input[p.cursorYPos])[:len([]rune(p.input[p.cursorYPos]))+p.cursorXPos], []rune(p.input[p.cursorYPos])[len([]rune(p.input[p.cursorYPos]))+p.cursorXPos+1:]...)) + p.cursorXPos++ + } else if p.cursorYPos < len(p.input)-1 { + p.input[p.cursorYPos] += p.input[p.cursorYPos+1] + appendAfterY := append([]string{}, p.input[p.cursorYPos+2:]...) + p.input = append(p.input[:p.cursorYPos+1], appendAfterY...) + p.cursorXPos = 0 + } + case keys.CtrlC: + os.Exit(0) + case keys.Down: + if p.cursorYPos+1 < len(p.input) { + p.cursorXPos = (internal.GetStringMaxWidth(p.input[p.cursorYPos]) + p.cursorXPos) - internal.GetStringMaxWidth(p.input[p.cursorYPos+1]) + if p.cursorXPos > 0 { + p.cursorXPos = 0 + } + p.cursorYPos++ + } + case keys.Up: + if p.cursorYPos > 0 { + p.cursorXPos = (internal.GetStringMaxWidth(p.input[p.cursorYPos]) + p.cursorXPos) - internal.GetStringMaxWidth(p.input[p.cursorYPos-1]) + if p.cursorXPos > 0 { + p.cursorXPos = 0 + } + p.cursorYPos-- + } + } + + if internal.GetStringMaxWidth(p.input[p.cursorYPos]) > 0 { + switch key.Code { + case keys.Right: + if p.cursorXPos < 0 { + p.cursorXPos++ + } else if p.cursorYPos < len(p.input)-1 { + p.cursorYPos++ + p.cursorXPos = -internal.GetStringMaxWidth(p.input[p.cursorYPos]) + } + case keys.Left: + if p.cursorXPos+internal.GetStringMaxWidth(p.input[p.cursorYPos]) > 0 { + p.cursorXPos-- + } else if p.cursorYPos > 0 { + p.cursorYPos-- + p.cursorXPos = 0 + } + } + } + + p.updateArea(area) + + return false, nil + }) + if err != nil { + return "", err + } + + for i, s := range p.input { + if i < len(p.input)-1 { + areaText += s + "\n" + } else { + areaText += s + } + } + + return strings.ReplaceAll(areaText, p.text, ""), nil +} + +func (p *InteractiveTextInputPrinter) updateArea(area *AreaPrinter) string { + if !p.MultiLine { + p.cursorYPos = 0 + } + areaText := p.text + for i, s := range p.input { + if i < len(p.input)-1 { + areaText += s + "\n" + } else { + areaText += s + } + } + if p.cursorXPos+internal.GetStringMaxWidth(p.input[p.cursorYPos]) < 1 { + p.cursorXPos = -internal.GetStringMaxWidth(p.input[p.cursorYPos]) + } + + cursor.StartOfLine() + area.Update(areaText) + cursor.Up(len(p.input) - p.cursorYPos) + cursor.StartOfLine() + if p.MultiLine { + cursor.Right(internal.GetStringMaxWidth(p.input[p.cursorYPos]) + p.cursorXPos) + } else { + cursor.Right(internal.GetStringMaxWidth(areaText) + p.cursorXPos) + } + return areaText +} From 2b2936ca446325419d6b6e2ac156eaa32d6d34a5 Mon Sep 17 00:00:00 2001 From: Marvin Wendt Date: Mon, 20 Jun 2022 08:09:05 +0200 Subject: [PATCH 14/25] bump golangci-lint --- .github/workflows/golangci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/golangci.yml b/.github/workflows/golangci.yml index 558b850b3..d824eb0b3 100644 --- a/.github/workflows/golangci.yml +++ b/.github/workflows/golangci.yml @@ -16,4 +16,4 @@ jobs: uses: golangci/golangci-lint-action@v3 with: # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version. - version: v1.39 + version: v1.46 From 92d44022b0f25c3968c8b39eb33ed3d6c46bfcac Mon Sep 17 00:00:00 2001 From: Marvin Wendt Date: Mon, 20 Jun 2022 08:10:08 +0200 Subject: [PATCH 15/25] fix golangci-lint workflow to work with 1.18 --- .github/workflows/golangci.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/golangci.yml b/.github/workflows/golangci.yml index d824eb0b3..57428ec3a 100644 --- a/.github/workflows/golangci.yml +++ b/.github/workflows/golangci.yml @@ -1,3 +1,5 @@ + + name: golangci-lint on: [ push, pull_request ] jobs: @@ -10,10 +12,9 @@ jobs: - name: Set up Go uses: actions/setup-go@v2 with: - go-version: 1.16 + go-version: 1.18 - name: golangci-lint uses: golangci/golangci-lint-action@v3 with: - # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version. - version: v1.46 + version: latest From f176e0bb8aca0291ef6bd0af21e493af8e172c92 Mon Sep 17 00:00:00 2001 From: Marvin Wendt Date: Mon, 20 Jun 2022 08:16:05 +0200 Subject: [PATCH 16/25] fixed linting --- interactive_confirm_printer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interactive_confirm_printer.go b/interactive_confirm_printer.go index 115c2b903..e2d3ce5be 100644 --- a/interactive_confirm_printer.go +++ b/interactive_confirm_printer.go @@ -85,7 +85,7 @@ func (p InteractiveConfirmPrinter) WithSuffixStyle(style *Style) *InteractiveCon func (p InteractiveConfirmPrinter) Show(text ...string) (bool, error) { var result bool - if text == nil || len(text) == 0 || text[0] == "" { + if len(text) == 0 || text[0] == "" { text = []string{"Please confirm"} } From e53e43c279e3c5e6e50a43bb77c7643e44abfe26 Mon Sep 17 00:00:00 2001 From: Marvin Wendt Date: Mon, 20 Jun 2022 08:22:39 +0200 Subject: [PATCH 17/25] fixed linting --- ci/main.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ci/main.go b/ci/main.go index 1ebd8bb0a..09bdcd6f8 100644 --- a/ci/main.go +++ b/ci/main.go @@ -15,13 +15,13 @@ import ( ) type Examples struct { - sync.Mutex - m map[string]string + mu sync.Mutex + m map[string]string } func (e *Examples) Add(name, content string) { - e.Lock() - defer e.Unlock() + e.mu.Lock() + defer e.mu.Unlock() e.m[name] = content } From 15cc2a415ea668c0debfb60b7893f8af472293fa Mon Sep 17 00:00:00 2001 From: Marvin Wendt Date: Mon, 20 Jun 2022 08:41:57 +0200 Subject: [PATCH 18/25] added better example to the interactive select printer --- _examples/interactive_select/demo/ci.go | 18 ++++++++++++++++-- _examples/interactive_select/demo/main.go | 14 +++++++++++++- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/_examples/interactive_select/demo/ci.go b/_examples/interactive_select/demo/ci.go index f629cea7d..434b4f4a5 100644 --- a/_examples/interactive_select/demo/ci.go +++ b/_examples/interactive_select/demo/ci.go @@ -14,10 +14,24 @@ func init() { if os.Getenv("CI") == "true" { go func() { time.Sleep(time.Second) - keyboard.SimulateKeyPress(keys.Down) + for i := 0; i < 10; i++ { + keyboard.SimulateKeyPress(keys.Down) + time.Sleep(time.Millisecond * 250) + } time.Sleep(time.Second) - keyboard.SimulateKeyPress(keys.Down) + + 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) }() } diff --git a/_examples/interactive_select/demo/main.go b/_examples/interactive_select/demo/main.go index cec96479f..705cbb3f0 100644 --- a/_examples/interactive_select/demo/main.go +++ b/_examples/interactive_select/demo/main.go @@ -1,10 +1,22 @@ package main import ( + "fmt" + "github.com/pterm/pterm" ) func main() { - selectedOption, _ := pterm.DefaultInteractiveSelect.WithOptions([]string{"Option 1", "Option 2", "Option 3", "Option 4"}).Show() + var options []string + + for i := 0; i < 100; i++ { + options = append(options, fmt.Sprintf("Option %d", i)) + } + + for i := 0; i < 5; i++ { + options = append(options, fmt.Sprintf("You can use fuzzy searching (%d)", i)) + } + + selectedOption, _ := pterm.DefaultInteractiveSelect.WithOptions(options).Show() pterm.Info.Printfln("Selected option: %s", pterm.Green(selectedOption)) } From 238d02accdb0c54aa7f3b118d4dea87b0ceb5052 Mon Sep 17 00:00:00 2001 From: Marvin Wendt Date: Mon, 20 Jun 2022 08:59:19 +0200 Subject: [PATCH 19/25] added hint to multiline text input --- interactive_textinput_printer.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/interactive_textinput_printer.go b/interactive_textinput_printer.go index b177e9769..412616286 100644 --- a/interactive_textinput_printer.go +++ b/interactive_textinput_printer.go @@ -50,9 +50,9 @@ func (p *InteractiveTextInputPrinter) Show(text ...string) (string, error) { } if p.MultiLine { - areaText = p.TextStyle.Sprint(text[0] + ": \n") + areaText = p.TextStyle.Sprintfln("%s %s :", text[0], ThemeDefault.SecondaryStyle.Sprint("[Press tab to submit]")) } else { - areaText = p.TextStyle.Sprint(text[0] + ": ") + areaText = p.TextStyle.Sprintf("%s: ", text[0]) } p.text = areaText area, err := DefaultArea.Start(areaText) @@ -64,7 +64,7 @@ func (p *InteractiveTextInputPrinter) Show(text ...string) (string, error) { cursor.Up(1) cursor.StartOfLine() if !p.MultiLine { - cursor.Right(len(RemoveColorFromString(text[0])) + 2) + cursor.Right(len(RemoveColorFromString(areaText))) } err = keyboard.Listen(func(key keys.Key) (stop bool, err error) { From 263b4728ae4f4da27b0eb1b444ec49e066da3ced Mon Sep 17 00:00:00 2001 From: Marvin Wendt Date: Mon, 20 Jun 2022 09:07:52 +0200 Subject: [PATCH 20/25] fixed multiline input example CI logic --- .../interactive_textinput/multi-line/ci.go | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/_examples/interactive_textinput/multi-line/ci.go b/_examples/interactive_textinput/multi-line/ci.go index 87cbaa44f..723ef7715 100644 --- a/_examples/interactive_textinput/multi-line/ci.go +++ b/_examples/interactive_textinput/multi-line/ci.go @@ -14,9 +14,13 @@ func init() { if os.Getenv("CI") == "true" { go func() { time.Sleep(time.Second) - input := "11111112222222\n" - for _, r := range []rune(input) { - keyboard.SimulateKeyPress(r) + input := "1111111\n2222222" + for _, r := range input { + if r == '\n' { + keyboard.SimulateKeyPress(keys.Enter) + } else { + keyboard.SimulateKeyPress(r) + } time.Sleep(time.Millisecond * 250) } @@ -30,8 +34,12 @@ func init() { keyboard.SimulateKeyPress(keys.Enter) time.Sleep(time.Millisecond * 500) input = "33333333\n4\n5555555" - for _, r := range []rune(input) { - keyboard.SimulateKeyPress(r) + for _, r := range input { + if r == '\n' { + keyboard.SimulateKeyPress(keys.Enter) + } else { + keyboard.SimulateKeyPress(r) + } time.Sleep(time.Millisecond * 250) } From c4d09053c05cee1db128fe4b66f214db8a62db1f Mon Sep 17 00:00:00 2001 From: Marvin Wendt Date: Mon, 20 Jun 2022 10:39:41 +0200 Subject: [PATCH 21/25] added `InteractiveMultiselect` --- _examples/interactive_multiselect/demo/ci.go | 38 +++ .../interactive_multiselect/demo/main.go | 22 ++ interactive_multiselect_printer.go | 322 ++++++++++++++++++ 3 files changed, 382 insertions(+) create mode 100644 _examples/interactive_multiselect/demo/ci.go create mode 100644 _examples/interactive_multiselect/demo/main.go create mode 100644 interactive_multiselect_printer.go diff --git a/_examples/interactive_multiselect/demo/ci.go b/_examples/interactive_multiselect/demo/ci.go new file mode 100644 index 000000000..434b4f4a5 --- /dev/null +++ b/_examples/interactive_multiselect/demo/ci.go @@ -0,0 +1,38 @@ +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) + time.Sleep(time.Millisecond * 250) + } + 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) + }() + } +} diff --git a/_examples/interactive_multiselect/demo/main.go b/_examples/interactive_multiselect/demo/main.go new file mode 100644 index 000000000..3f119ec41 --- /dev/null +++ b/_examples/interactive_multiselect/demo/main.go @@ -0,0 +1,22 @@ +package main + +import ( + "fmt" + + "github.com/pterm/pterm" +) + +func main() { + var options []string + + for i := 0; i < 100; i++ { + options = append(options, fmt.Sprintf("Option %d", i)) + } + + for i := 0; i < 5; i++ { + options = append(options, fmt.Sprintf("You can use fuzzy searching (%d)", i)) + } + + selectedOptions, _ := pterm.DefaultInteractiveMultiselect.WithOptions(options).Show() + pterm.Info.Printfln("Selected options: %s", pterm.Green(selectedOptions)) +} diff --git a/interactive_multiselect_printer.go b/interactive_multiselect_printer.go new file mode 100644 index 000000000..e8c4efeca --- /dev/null +++ b/interactive_multiselect_printer.go @@ -0,0 +1,322 @@ +package pterm + +import ( + "fmt" + "os" + "sort" + + "atomicgo.dev/cursor" + "atomicgo.dev/keyboard" + "atomicgo.dev/keyboard/keys" + "github.com/lithammer/fuzzysearch/fuzzy" +) + +var ( + // DefaultInteractiveMultiselect is the default InteractiveMultiselect printer. + DefaultInteractiveMultiselect = InteractiveMultiselectPrinter{ + TextStyle: &ThemeDefault.PrimaryStyle, + DefaultText: "Please select your options", + Options: []string{}, + OptionStyle: &ThemeDefault.DefaultText, + DefaultOptions: []string{}, + MaxHeight: 5, + Selector: ">", + SelectorStyle: &ThemeDefault.SecondaryStyle, + } +) + +// InteractiveMultiselectPrinter is a printer for interactive select menus. +type InteractiveMultiselectPrinter struct { + DefaultText string + TextStyle *Style + Options []string + OptionStyle *Style + DefaultOptions []string + MaxHeight int + Selector string + SelectorStyle *Style + + selectedOption int + selectedOptions []int + text string + fuzzySearchString string + fuzzySearchMatches []string + displayedOptions []string + displayedOptionsStart int + displayedOptionsEnd int +} + +// WithOptions sets the options. +func (p InteractiveMultiselectPrinter) WithOptions(options []string) *InteractiveMultiselectPrinter { + p.Options = options + return &p +} + +// WithDefaultOptions sets the default options. +func (p InteractiveMultiselectPrinter) WithDefaultOptions(options []string) *InteractiveMultiselectPrinter { + p.DefaultOptions = options + return &p +} + +// WithDefaultText sets the default text. +func (p InteractiveMultiselectPrinter) WithDefaultText(text string) *InteractiveMultiselectPrinter { + p.DefaultText = text + return &p +} + +// WithMaxHeight sets the maximum height of the select menu. +func (p InteractiveMultiselectPrinter) WithMaxHeight(maxHeight int) *InteractiveMultiselectPrinter { + p.MaxHeight = maxHeight + return &p +} + +// Show shows the interactive select menu and returns the selected entry. +func (p *InteractiveMultiselectPrinter) Show(text ...string) ([]string, error) { + if len(text) == 0 || Sprint(text[0]) == "" { + text = []string{p.DefaultText} + } + + p.text = p.TextStyle.Sprint(text[0]) + p.fuzzySearchMatches = append([]string{}, p.Options...) + + if p.MaxHeight == 0 { + p.MaxHeight = DefaultInteractiveMultiselect.MaxHeight + } + + maxHeight := p.MaxHeight + if maxHeight > len(p.fuzzySearchMatches) { + maxHeight = len(p.fuzzySearchMatches) + } + + if len(p.Options) == 0 { + return nil, fmt.Errorf("no options provided") + } + + p.displayedOptions = append([]string{}, p.fuzzySearchMatches[:maxHeight]...) + p.displayedOptionsStart = 0 + p.displayedOptionsEnd = maxHeight + + for _, option := range p.DefaultOptions { + p.selectOption(option) + } + + area, err := DefaultArea.Start(p.renderSelectMenu()) + defer area.Stop() + if err != nil { + return nil, fmt.Errorf("could not start area: %w", err) + } + + area.Update(p.renderSelectMenu()) + + cursor.Hide() + defer cursor.Show() + err = keyboard.Listen(func(keyInfo keys.Key) (stop bool, err error) { + key := keyInfo.Code + + if p.MaxHeight > len(p.fuzzySearchMatches) { + maxHeight = len(p.fuzzySearchMatches) + } else { + maxHeight = p.MaxHeight + } + + switch key { + case keys.RuneKey: + // Fuzzy search for options + // append to fuzzy search string + p.fuzzySearchString += keyInfo.String() + p.selectedOption = 0 + p.displayedOptionsStart = 0 + p.displayedOptionsEnd = maxHeight + p.displayedOptions = append([]string{}, p.fuzzySearchMatches[:maxHeight]...) + area.Update(p.renderSelectMenu()) + case keys.Tab: + if len(p.fuzzySearchMatches) == 0 { + return false, nil + } + area.Update(p.renderFinishedMenu()) + return true, nil + case keys.Space: + p.fuzzySearchString += " " + p.selectedOption = 0 + area.Update(p.renderSelectMenu()) + case keys.Backspace: + // Remove last character from fuzzy search string + if len(p.fuzzySearchString) > 0 { + // Handle UTF-8 characters + p.fuzzySearchString = string([]rune(p.fuzzySearchString)[:len([]rune(p.fuzzySearchString))-1]) + } + + if p.fuzzySearchString == "" { + p.fuzzySearchMatches = append([]string{}, p.Options...) + } + + p.renderSelectMenu() + + if len(p.fuzzySearchMatches) > p.MaxHeight { + maxHeight = p.MaxHeight + } else { + maxHeight = len(p.fuzzySearchMatches) + } + + p.selectedOption = 0 + p.displayedOptionsStart = 0 + p.displayedOptionsEnd = maxHeight + p.displayedOptions = append([]string{}, p.fuzzySearchMatches[p.displayedOptionsStart:p.displayedOptionsEnd]...) + + area.Update(p.renderSelectMenu()) + case keys.Up: + if len(p.fuzzySearchMatches) == 0 { + return false, nil + } + if p.selectedOption > 0 { + p.selectedOption-- + if p.selectedOption < p.displayedOptionsStart { + p.displayedOptionsStart-- + p.displayedOptionsEnd-- + if p.displayedOptionsStart < 0 { + p.displayedOptionsStart = 0 + p.displayedOptionsEnd = maxHeight + } + p.displayedOptions = append([]string{}, p.fuzzySearchMatches[p.displayedOptionsStart:p.displayedOptionsEnd]...) + } + } else { + p.selectedOption = len(p.fuzzySearchMatches) - 1 + p.displayedOptionsStart = len(p.fuzzySearchMatches) - maxHeight + p.displayedOptionsEnd = len(p.fuzzySearchMatches) + p.displayedOptions = append([]string{}, p.fuzzySearchMatches[p.displayedOptionsStart:p.displayedOptionsEnd]...) + } + + area.Update(p.renderSelectMenu()) + case keys.Down: + if len(p.fuzzySearchMatches) == 0 { + return false, nil + } + p.displayedOptions = p.fuzzySearchMatches[:maxHeight] + if p.selectedOption < len(p.fuzzySearchMatches)-1 { + p.selectedOption++ + if p.selectedOption >= p.displayedOptionsEnd { + p.displayedOptionsStart++ + p.displayedOptionsEnd++ + p.displayedOptions = append([]string{}, p.fuzzySearchMatches[p.displayedOptionsStart:p.displayedOptionsEnd]...) + } + } else { + p.selectedOption = 0 + p.displayedOptionsStart = 0 + p.displayedOptionsEnd = maxHeight + p.displayedOptions = append([]string{}, p.fuzzySearchMatches[p.displayedOptionsStart:p.displayedOptionsEnd]...) + } + + area.Update(p.renderSelectMenu()) + case keys.CtrlC: + os.Exit(1) + case keys.Enter: + // Select option if not already selected + p.selectOption(p.fuzzySearchMatches[p.selectedOption]) + area.Update(p.renderSelectMenu()) + } + + return false, nil + }) + if err != nil { + fmt.Println(err) + return nil, fmt.Errorf("failed to start keyboard listener: %w", err) + } + + var result []string + for _, selectedOption := range p.selectedOptions { + result = append(result, p.Options[selectedOption]) + } + + return result, nil +} + +func (p InteractiveMultiselectPrinter) findOptionByText(text string) int { + for i, option := range p.Options { + if option == text { + return i + } + } + return -1 +} + +func (p *InteractiveMultiselectPrinter) isSelected(optionText string) bool { + for _, selectedOption := range p.selectedOptions { + if p.Options[selectedOption] == optionText { + return true + } + } + + return false +} + +func (p *InteractiveMultiselectPrinter) selectOption(optionText string) { + if p.isSelected(optionText) { + // Remove from selected options + for i, selectedOption := range p.selectedOptions { + if p.Options[selectedOption] == optionText { + p.selectedOptions = append(p.selectedOptions[:i], p.selectedOptions[i+1:]...) + break + } + } + } else { + // Add to selected options + p.selectedOptions = append(p.selectedOptions, p.findOptionByText(optionText)) + } +} + +func (p *InteractiveMultiselectPrinter) renderSelectMenu() string { + var content string + content += Sprintf("%s %s: %s\n", p.text, ThemeDefault.SecondaryStyle.Sprint("[type to search]"), p.fuzzySearchString) + + // find options that match fuzzy search string + rankedResults := fuzzy.RankFindFold(p.fuzzySearchString, p.Options) + // map rankedResults to fuzzySearchMatches + p.fuzzySearchMatches = []string{} + if len(rankedResults) != len(p.Options) { + sort.Sort(rankedResults) + } + for _, result := range rankedResults { + p.fuzzySearchMatches = append(p.fuzzySearchMatches, result.Target) + } + + indexMapper := make([]string, len(p.fuzzySearchMatches)) + for i := 0; i < len(p.fuzzySearchMatches); i++ { + // if in displayed options range + if i >= p.displayedOptionsStart && i < p.displayedOptionsEnd { + indexMapper[i] = p.fuzzySearchMatches[i] + } + } + + for i, option := range indexMapper { + if option == "" { + continue + } + var checkmark string + if p.isSelected(option) { + checkmark = fmt.Sprintf("[%s]", Green("✓")) + } else { + checkmark = fmt.Sprintf("[%s]", Red("✗")) + } + if i == p.selectedOption { + content += Sprintf("%s %s %s\n", p.renderSelector(), checkmark, option) + } else { + content += Sprintf(" %s %s\n", checkmark, option) + } + } + + return content +} + +func (p InteractiveMultiselectPrinter) renderFinishedMenu() string { + var content string + content += Sprintf("%s: %s\n", p.text, p.fuzzySearchString) + for _, option := range p.selectedOptions { + content += Sprintf(" %s %s\n", p.renderSelector(), p.Options[option]) + } + + return content +} + +func (p InteractiveMultiselectPrinter) renderSelector() string { + return p.SelectorStyle.Sprint(p.Selector) +} From a9b81a78f0ab3844e978a73e02f8570ec7f0cbd4 Mon Sep 17 00:00:00 2001 From: Marvin Wendt Date: Mon, 20 Jun 2022 10:43:47 +0200 Subject: [PATCH 22/25] added `WithDefaultText` --- interactive_select_printer.go | 10 +++++++++- interactive_textinput_printer.go | 16 ++++++++++++---- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/interactive_select_printer.go b/interactive_select_printer.go index ad2d67023..c9fe5ded6 100644 --- a/interactive_select_printer.go +++ b/interactive_select_printer.go @@ -15,6 +15,7 @@ var ( // DefaultInteractiveSelect is the default InteractiveSelect printer. DefaultInteractiveSelect = InteractiveSelectPrinter{ TextStyle: &ThemeDefault.PrimaryStyle, + DefaultText: "Please select an option", Options: []string{}, OptionStyle: &ThemeDefault.DefaultText, DefaultOption: "", @@ -27,6 +28,7 @@ var ( // InteractiveSelectPrinter is a printer for interactive select menus. type InteractiveSelectPrinter struct { TextStyle *Style + DefaultText string Options []string OptionStyle *Style DefaultOption string @@ -44,6 +46,12 @@ type InteractiveSelectPrinter struct { displayedOptionsEnd int } +// WithDefaultText sets the default text. +func (p InteractiveSelectPrinter) WithDefaultText(text string) *InteractiveSelectPrinter { + p.DefaultText = text + return &p +} + // WithOptions sets the options. func (p InteractiveSelectPrinter) WithOptions(options []string) *InteractiveSelectPrinter { p.Options = options @@ -65,7 +73,7 @@ func (p InteractiveSelectPrinter) WithMaxHeight(maxHeight int) *InteractiveSelec // Show shows the interactive select menu and returns the selected entry. func (p *InteractiveSelectPrinter) Show(text ...string) (string, error) { if len(text) == 0 || Sprint(text[0]) == "" { - text = []string{"Please select an option"} + text = []string{p.DefaultText} } p.text = p.TextStyle.Sprint(text[0]) diff --git a/interactive_textinput_printer.go b/interactive_textinput_printer.go index 412616286..f6c682405 100644 --- a/interactive_textinput_printer.go +++ b/interactive_textinput_printer.go @@ -14,14 +14,16 @@ import ( var ( // DefaultInteractiveTextInput is the default InteractiveTextInput printer. DefaultInteractiveTextInput = InteractiveTextInputPrinter{ - TextStyle: &ThemeDefault.PrimaryStyle, + DefaultText: "Input text", + TextStyle: &ThemeDefault.PrimaryStyle, } ) // InteractiveTextInputPrinter is a printer for interactive select menus. type InteractiveTextInputPrinter struct { - TextStyle *Style - MultiLine bool + TextStyle *Style + DefaultText string + MultiLine bool input []string cursorXPos int @@ -29,6 +31,12 @@ type InteractiveTextInputPrinter struct { text string } +// WithDefaultText sets the default text. +func (p *InteractiveTextInputPrinter) WithDefaultText(text string) *InteractiveTextInputPrinter { + p.DefaultText = text + return p +} + // WithTextStyle sets the text style. func (p *InteractiveTextInputPrinter) WithTextStyle(style *Style) *InteractiveTextInputPrinter { p.TextStyle = style @@ -46,7 +54,7 @@ func (p *InteractiveTextInputPrinter) Show(text ...string) (string, error) { var areaText string if len(text) == 0 || Sprint(text[0]) == "" { - text = []string{"Input text"} + text = []string{p.DefaultText} } if p.MultiLine { From 22dee76ded389c59cda27d761e46a057ff9f9c8a Mon Sep 17 00:00:00 2001 From: Marvin Wendt Date: Mon, 20 Jun 2022 11:28:59 +0200 Subject: [PATCH 23/25] changed test coverage to informational --- codecov.yml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 codecov.yml diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 000000000..bfdc9877d --- /dev/null +++ b/codecov.yml @@ -0,0 +1,8 @@ +coverage: + status: + project: + default: + informational: true + patch: + default: + informational: true From 0609e02bfab58879be3d69be756e22f5d0ada255 Mon Sep 17 00:00:00 2001 From: Marvin Wendt Date: Mon, 20 Jun 2022 11:29:11 +0200 Subject: [PATCH 24/25] added tests --- _examples/interactive_multiselect/demo/ci.go | 8 +++- interactive_confirm_printer.go | 10 ++++- interactive_confirm_printer_test.go | 40 ++++++++++++++++++++ interactive_multiselect_printer.go | 2 +- interactive_select_printer_test.go | 6 +++ interactive_textinput_printer_test.go | 30 +++++++++++++++ 6 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 interactive_textinput_printer_test.go diff --git a/_examples/interactive_multiselect/demo/ci.go b/_examples/interactive_multiselect/demo/ci.go index 434b4f4a5..f8e4cd084 100644 --- a/_examples/interactive_multiselect/demo/ci.go +++ b/_examples/interactive_multiselect/demo/ci.go @@ -16,7 +16,11 @@ func init() { time.Sleep(time.Second) for i := 0; i < 10; i++ { keyboard.SimulateKeyPress(keys.Down) - time.Sleep(time.Millisecond * 250) + if i%2 == 0 { + time.Sleep(time.Millisecond * 100) + keyboard.SimulateKeyPress(keys.Enter) + } + time.Sleep(time.Millisecond * 500) } time.Sleep(time.Second) @@ -33,6 +37,8 @@ func init() { } keyboard.SimulateKeyPress(keys.Enter) + time.Sleep(time.Millisecond * 350) + keyboard.SimulateKeyPress(keys.Tab) }() } } diff --git a/interactive_confirm_printer.go b/interactive_confirm_printer.go index e2d3ce5be..cbfa53692 100644 --- a/interactive_confirm_printer.go +++ b/interactive_confirm_printer.go @@ -15,6 +15,7 @@ var ( // Pressing enter without typing "y" or "n" will return the configured default value (by default set to "no"). DefaultInteractiveConfirm = InteractiveConfirmPrinter{ DefaultValue: false, + DefaultText: "Please confirm", TextStyle: &ThemeDefault.PrimaryStyle, ConfirmText: "Yes", ConfirmStyle: &ThemeDefault.SuccessMessageStyle, @@ -27,6 +28,7 @@ var ( // InteractiveConfirmPrinter is a printer for interactive confirm prompts. type InteractiveConfirmPrinter struct { DefaultValue bool + DefaultText string TextStyle *Style ConfirmText string ConfirmStyle *Style @@ -35,6 +37,12 @@ type InteractiveConfirmPrinter struct { SuffixStyle *Style } +// WithDefaultText sets the default text. +func (p InteractiveConfirmPrinter) WithDefaultText(text string) *InteractiveConfirmPrinter { + p.DefaultText = text + return &p +} + // WithDefaultValue sets the default value, which will be returned when the user presses enter without typing "y" or "n". func (p InteractiveConfirmPrinter) WithDefaultValue(value bool) *InteractiveConfirmPrinter { p.DefaultValue = value @@ -86,7 +94,7 @@ func (p InteractiveConfirmPrinter) Show(text ...string) (bool, error) { var result bool if len(text) == 0 || text[0] == "" { - text = []string{"Please confirm"} + text = []string{p.DefaultText} } p.TextStyle.Print(text[0] + " " + p.getSuffix() + ": ") diff --git a/interactive_confirm_printer_test.go b/interactive_confirm_printer_test.go index 74a111df9..ec6dbb963 100644 --- a/interactive_confirm_printer_test.go +++ b/interactive_confirm_printer_test.go @@ -6,6 +6,7 @@ import ( "atomicgo.dev/keyboard" "atomicgo.dev/keyboard/keys" "github.com/MarvinJWendt/testza" + "github.com/pterm/pterm" ) @@ -47,3 +48,42 @@ func TestInteractiveConfirmPrinter_WithDefaultValue_true(t *testing.T) { result, _ := p.Show() testza.AssertTrue(t, result) } + +func TestInteractiveConfirmPrinter_WithConfirmStyle(t *testing.T) { + style := pterm.NewStyle(pterm.FgRed) + p := pterm.DefaultInteractiveConfirm.WithConfirmStyle(style) + testza.AssertEqual(t, p.ConfirmStyle, style) +} + +func TestInteractiveConfirmPrinter_WithConfirmText(t *testing.T) { + p := pterm.DefaultInteractiveConfirm.WithConfirmText("confirm") + testza.AssertEqual(t, p.ConfirmText, "confirm") +} + +func TestInteractiveConfirmPrinter_WithDefaultText(t *testing.T) { + p := pterm.DefaultInteractiveConfirm.WithDefaultText("default") + testza.AssertEqual(t, p.DefaultText, "default") +} + +func TestInteractiveConfirmPrinter_WithRejectStyle(t *testing.T) { + style := pterm.NewStyle(pterm.FgRed) + p := pterm.DefaultInteractiveConfirm.WithRejectStyle(style) + testza.AssertEqual(t, p.RejectStyle, style) +} + +func TestInteractiveConfirmPrinter_WithRejectText(t *testing.T) { + p := pterm.DefaultInteractiveConfirm.WithRejectText("reject") + testza.AssertEqual(t, p.RejectText, "reject") +} + +func TestInteractiveConfirmPrinter_WithSuffixStyle(t *testing.T) { + style := pterm.NewStyle(pterm.FgRed) + p := pterm.DefaultInteractiveConfirm.WithSuffixStyle(style) + testza.AssertEqual(t, p.SuffixStyle, style) +} + +func TestInteractiveConfirmPrinter_WithTextStyle(t *testing.T) { + style := pterm.NewStyle(pterm.FgRed) + p := pterm.DefaultInteractiveConfirm.WithTextStyle(style) + testza.AssertEqual(t, p.TextStyle, style) +} diff --git a/interactive_multiselect_printer.go b/interactive_multiselect_printer.go index e8c4efeca..5667ea909 100644 --- a/interactive_multiselect_printer.go +++ b/interactive_multiselect_printer.go @@ -266,7 +266,7 @@ func (p *InteractiveMultiselectPrinter) selectOption(optionText string) { func (p *InteractiveMultiselectPrinter) renderSelectMenu() string { var content string - content += Sprintf("%s %s: %s\n", p.text, ThemeDefault.SecondaryStyle.Sprint("[type to search]"), p.fuzzySearchString) + content += Sprintf("%s %s: %s\n", p.text, ThemeDefault.SecondaryStyle.Sprint("[select with enter, type to search, confirm with tab]"), p.fuzzySearchString) // find options that match fuzzy search string rankedResults := fuzzy.RankFindFold(p.fuzzySearchString, p.Options) diff --git a/interactive_select_printer_test.go b/interactive_select_printer_test.go index 53b7a2a4c..35026f8e3 100644 --- a/interactive_select_printer_test.go +++ b/interactive_select_printer_test.go @@ -6,6 +6,7 @@ import ( "atomicgo.dev/keyboard" "atomicgo.dev/keyboard/keys" "github.com/MarvinJWendt/testza" + "github.com/pterm/pterm" ) @@ -19,6 +20,11 @@ func TestInteractiveSelectPrinter_Show(t *testing.T) { testza.AssertEqual(t, "d", result) } +func TestInteractiveSelectPrinter_WithDefaultText(t *testing.T) { + p := pterm.DefaultInteractiveSelect.WithDefaultText("default") + testza.AssertEqual(t, p.DefaultText, "default") +} + func TestInteractiveSelectPrinter_WithDefaultOption(t *testing.T) { p := pterm.DefaultInteractiveSelect.WithDefaultOption("default") testza.AssertEqual(t, p.DefaultOption, "default") diff --git a/interactive_textinput_printer_test.go b/interactive_textinput_printer_test.go new file mode 100644 index 000000000..1b0d9523d --- /dev/null +++ b/interactive_textinput_printer_test.go @@ -0,0 +1,30 @@ +package pterm_test + +import ( + "testing" + + "github.com/MarvinJWendt/testza" + + "github.com/pterm/pterm" +) + +func TestInteractiveTextInputPrinter_WithDefaultText(t *testing.T) { + p := pterm.DefaultInteractiveTextInput.WithDefaultText("default") + testza.AssertEqual(t, p.DefaultText, "default") +} + +func TestInteractiveTextInputPrinter_WithMultiLine_true(t *testing.T) { + p := pterm.DefaultInteractiveTextInput.WithMultiLine() + testza.AssertTrue(t, p.MultiLine) +} + +func TestInteractiveTextInputPrinter_WithMultiLine_false(t *testing.T) { + p := pterm.DefaultInteractiveTextInput.WithMultiLine(false) + testza.AssertFalse(t, p.MultiLine) +} + +func TestInteractiveTextInputPrinter_WithTextStyle(t *testing.T) { + style := pterm.NewStyle(pterm.FgRed) + p := pterm.DefaultInteractiveTextInput.WithTextStyle(style) + testza.AssertEqual(t, p.TextStyle, style) +} From ae51bcb0e5da1fe618bd22a2cb17e3c57651ecc9 Mon Sep 17 00:00:00 2001 From: Marvin Wendt Date: Mon, 20 Jun 2022 11:33:12 +0200 Subject: [PATCH 25/25] added docs --- interactive_multiselect_printer.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interactive_multiselect_printer.go b/interactive_multiselect_printer.go index 5667ea909..b1ab8fec7 100644 --- a/interactive_multiselect_printer.go +++ b/interactive_multiselect_printer.go @@ -25,7 +25,7 @@ var ( } ) -// InteractiveMultiselectPrinter is a printer for interactive select menus. +// InteractiveMultiselectPrinter is a printer for interactive multiselect menus. type InteractiveMultiselectPrinter struct { DefaultText string TextStyle *Style @@ -70,7 +70,7 @@ func (p InteractiveMultiselectPrinter) WithMaxHeight(maxHeight int) *Interactive return &p } -// Show shows the interactive select menu and returns the selected entry. +// Show shows the interactive multiselect menu and returns the selected entry. func (p *InteractiveMultiselectPrinter) Show(text ...string) ([]string, error) { if len(text) == 0 || Sprint(text[0]) == "" { text = []string{p.DefaultText}