diff --git a/_examples/interactive_multiselect/custom-keys/ci.go b/_examples/interactive_multiselect/custom-keys/ci.go new file mode 100644 index 000000000..1fedeeae1 --- /dev/null +++ b/_examples/interactive_multiselect/custom-keys/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/custom-keys/main.go b/_examples/interactive_multiselect/custom-keys/main.go new file mode 100644 index 000000000..ba515001e --- /dev/null +++ b/_examples/interactive_multiselect/custom-keys/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 941cddad3..dc6be6b2b 100644 --- a/interactive_multiselect_printer.go +++ b/interactive_multiselect_printer.go @@ -22,6 +22,9 @@ var ( MaxHeight: 5, Selector: ">", SelectorStyle: &ThemeDefault.SecondaryStyle, + Filter: true, + KeySelect: keys.Enter, + KeyConfirm: keys.Tab, } ) @@ -35,6 +38,7 @@ type InteractiveMultiselectPrinter struct { MaxHeight int Selector string SelectorStyle *Style + Filter bool selectedOption int selectedOptions []int @@ -44,6 +48,9 @@ type InteractiveMultiselectPrinter struct { displayedOptions []string displayedOptionsStart int displayedOptionsEnd int + + KeySelect keys.KeyCode + KeyConfirm keys.KeyCode } // WithOptions sets the options. @@ -70,6 +77,24 @@ func (p InteractiveMultiselectPrinter) WithMaxHeight(maxHeight int) *Interactive return &p } +// WithFilter sets the Filter option +func (p InteractiveMultiselectPrinter) WithFilter(filter bool) *InteractiveMultiselectPrinter { + p.Filter = filter + 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 @@ -111,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() @@ -125,25 +154,35 @@ func (p *InteractiveMultiselectPrinter) 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 = maxHeight - p.displayedOptions = append([]string{}, p.fuzzySearchMatches[:maxHeight]...) - area.Update(p.renderSelectMenu()) - case keys.Tab: + case p.KeyConfirm: if len(p.fuzzySearchMatches) == 0 { return false, nil } area.Update(p.renderFinishedMenu()) return true, nil - case keys.Space: - p.fuzzySearchString += " " - p.selectedOption = 0 + 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 + // 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.Space: + 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 { @@ -226,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 @@ -323,7 +356,11 @@ func (p *InteractiveMultiselectPrinter) renderSelectMenu() string { } } - 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")) + 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 { + help += fmt.Sprintf("| type to %s", Bold.Sprint("filter")) + } + content += ThemeDefault.SecondaryStyle.Sprintfln(help) return content }