diff --git a/_examples/interactive_multiselect/demo_custom/README.md b/_examples/interactive_multiselect/demo_custom/README.md new file mode 100644 index 000000000..7b8ff37fd --- /dev/null +++ b/_examples/interactive_multiselect/demo_custom/README.md @@ -0,0 +1,29 @@ +# interactive_multiselect/demo_custom + +![Animation](animation.svg) + +```go +package main + +import ( + "fmt" + + "atomicgo.dev/keyboard/keys" + "github.com/pterm/pterm" +) + +func main() { + var options []string + + for i := 0; i < 5; i++ { + options = append(options, fmt.Sprintf("Option %d", i)) + } + + printer := pterm.DefaultInteractiveMultiselect.WithOptions(options) + printer.NoFilter = true + printer.KeyConfirm = keys.Enter + printer.KeySelect = keys.Space + selectedOptions, _ := printer.Show() + pterm.Info.Printfln("Selected options: %s", pterm.Green(selectedOptions)) +} +``` diff --git a/_examples/interactive_multiselect/demo_custom/animation.svg b/_examples/interactive_multiselect/demo_custom/animation.svg new file mode 100644 index 000000000..3d8d397ec --- /dev/null +++ b/_examples/interactive_multiselect/demo_custom/animation.svg @@ -0,0 +1,10 @@ +Pleaseselectyouroptions:>[]Option0[]Option1[]Option2[]Option3[]Option4enter:select|tab:confirm|left:none|right:all|typetofilter[]Option0>[]Option1>[]Option1[]Option1>[]Option2>[]Option3[]Option3>[]Option5>[]Option5[]Option5>[]Option6[]Option6[]Option7>[]Option8[]Option8Pleaseselectyouroptions:f>[]Youcanusefuzzysearching(0)[]Youcanusefuzzysearching(1)[]Youcanusefuzzysearching(2)[]Youcanusefuzzysearching(3)[]Youcanusefuzzysearching(4)Pleaseselectyouroptions:fuzzy[]Youcanusefuzzysearching(0)>[]Youcanusefuzzysearching(2)>Option1>Option3>Option5>Option7>Option9>Youcanusefuzzysearching(2) INFO Selectedoptions:[Option1Option3Option5Option7Option9Youcanusefuzzysearching(2)]>[]Option3>[]Option4>[]Option7>[]Option7>[]Option9>[]Option9[]Option9>[]Option10Pleaseselectyouroptions:fuPleaseselectyouroptions:fuzPleaseselectyouroptions:fuzz>[]Youcanusefuzzysearching(1)>[]Youcanusefuzzysearching(2)Restartinganimation... \ No newline at end of file diff --git a/_examples/interactive_multiselect/demo_custom/ci.go b/_examples/interactive_multiselect/demo_custom/ci.go new file mode 100644 index 000000000..1fedeeae1 --- /dev/null +++ b/_examples/interactive_multiselect/demo_custom/ci.go @@ -0,0 +1,31 @@ +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.Millisecond * 100) + keyboard.SimulateKeyPress(keys.Space) + + time.Sleep(time.Millisecond * 300) + + keyboard.SimulateKeyPress(keys.Down) + time.Sleep(time.Millisecond * 100) + keyboard.SimulateKeyPress(keys.Space) + + time.Sleep(time.Millisecond * 300) + keyboard.SimulateKeyPress(keys.Enter) + }() + } +} diff --git a/_examples/interactive_multiselect/demo_custom/main.go b/_examples/interactive_multiselect/demo_custom/main.go new file mode 100644 index 000000000..ba515001e --- /dev/null +++ b/_examples/interactive_multiselect/demo_custom/main.go @@ -0,0 +1,23 @@ +package main + +import ( + "fmt" + + "atomicgo.dev/keyboard/keys" + "github.com/pterm/pterm" +) + +func main() { + var options []string + + for i := 0; i < 5; i++ { + options = append(options, fmt.Sprintf("Option %d", i)) + } + + printer := pterm.DefaultInteractiveMultiselect.WithOptions(options) + printer.Filter = false + printer.KeyConfirm = keys.Enter + printer.KeySelect = keys.Space + selectedOptions, _ := printer.Show() + pterm.Info.Printfln("Selected options: %s", pterm.Green(selectedOptions)) +} diff --git a/interactive_multiselect_printer.go b/interactive_multiselect_printer.go index ee15e6d6f..dc6be6b2b 100644 --- a/interactive_multiselect_printer.go +++ b/interactive_multiselect_printer.go @@ -23,6 +23,8 @@ var ( Selector: ">", SelectorStyle: &ThemeDefault.SecondaryStyle, Filter: true, + KeySelect: keys.Enter, + KeyConfirm: keys.Tab, } ) @@ -46,6 +48,9 @@ type InteractiveMultiselectPrinter struct { displayedOptions []string displayedOptionsStart int displayedOptionsEnd int + + KeySelect keys.KeyCode + KeyConfirm keys.KeyCode } // WithOptions sets the options. @@ -78,6 +83,18 @@ func (p InteractiveMultiselectPrinter) WithFilter(filter bool) *InteractiveMulti return &p } +// WithKeySelect sets the confirm key +func (p InteractiveMultiselectPrinter) WithKeySelect(keySelect keys.KeyCode) *InteractiveMultiselectPrinter { + p.KeySelect = keySelect + return &p +} + +// WithKeyConfirm sets the confirm key +func (p InteractiveMultiselectPrinter) WithKeyConfirm(keyConfirm keys.KeyCode) *InteractiveMultiselectPrinter { + p.KeyConfirm = keyConfirm + return &p +} + // Show shows the interactive multiselect menu and returns the selected entry. func (p *InteractiveMultiselectPrinter) Show(text ...string) ([]string, error) { // should be the first defer statement to make sure it is executed last @@ -119,6 +136,10 @@ func (p *InteractiveMultiselectPrinter) Show(text ...string) ([]string, error) { return nil, fmt.Errorf("could not start area: %w", err) } + if p.Filter && (p.KeyConfirm == keys.Space || p.KeySelect == keys.Space) { + return nil, fmt.Errorf("if filter/search is active, keys.Space can not be used for KeySelect or KeyConfirm") + } + area.Update(p.renderSelectMenu()) cursor.Hide() @@ -133,6 +154,18 @@ func (p *InteractiveMultiselectPrinter) Show(text ...string) ([]string, error) { } switch key { + case p.KeyConfirm: + if len(p.fuzzySearchMatches) == 0 { + return false, nil + } + area.Update(p.renderFinishedMenu()) + return true, nil + case p.KeySelect: + if len(p.fuzzySearchMatches) > 0 { + // Select option if not already selected + p.selectOption(p.fuzzySearchMatches[p.selectedOption]) + } + area.Update(p.renderSelectMenu()) case keys.RuneKey: if p.Filter { // Fuzzy search for options @@ -144,16 +177,12 @@ func (p *InteractiveMultiselectPrinter) Show(text ...string) ([]string, error) { 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()) + if p.Filter { + p.fuzzySearchString += " " + p.selectedOption = 0 + area.Update(p.renderSelectMenu()) + } case keys.Backspace: // Remove last character from fuzzy search string if len(p.fuzzySearchString) > 0 { @@ -236,12 +265,6 @@ func (p *InteractiveMultiselectPrinter) Show(text ...string) ([]string, error) { case keys.CtrlC: cancel() return true, nil - case keys.Enter: - if len(p.fuzzySearchMatches) > 0 { - // Select option if not already selected - p.selectOption(p.fuzzySearchMatches[p.selectedOption]) - } - area.Update(p.renderSelectMenu()) } return false, nil @@ -333,11 +356,11 @@ func (p *InteractiveMultiselectPrinter) renderSelectMenu() string { } } + help := fmt.Sprintf("%s: %s | %s: %s | left: %s | right: %s", p.KeySelect, Bold.Sprint("select"), p.KeyConfirm, Bold.Sprint("confirm"), Bold.Sprint("none"), Bold.Sprint("all")) if p.Filter { - content += ThemeDefault.SecondaryStyle.Sprintfln("enter: %s | tab: %s | left: %s | right: %s | type to %s", Bold.Sprint("select"), Bold.Sprint("confirm"), Bold.Sprint("none"), Bold.Sprint("all"), Bold.Sprint("filter")) - } else { - content += ThemeDefault.SecondaryStyle.Sprintfln("enter: %s | tab: %s | left: %s | right: %s", Bold.Sprint("select"), Bold.Sprint("confirm"), Bold.Sprint("none"), Bold.Sprint("all")) + help += fmt.Sprintf("| type to %s", Bold.Sprint("filter")) } + content += ThemeDefault.SecondaryStyle.Sprintfln(help) return content }