diff --git a/.assets/generate.py b/.assets/generate.py index a5fae5c..0c4bf98 100755 --- a/.assets/generate.py +++ b/.assets/generate.py @@ -65,7 +65,12 @@ def process_tape(tape: Path) -> str: def main() -> None: - tapes = sorted(ASSETS_DIR.glob("*.tape")) + selector = ( + "*.tape" if sys.argv == 0 else sys.argv[1].removesuffix(".tape") + ".tape" + ) + print(selector) + + tapes = sorted(ASSETS_DIR.glob(selector)) if not tapes: print("No .tape files found in", ASSETS_DIR) sys.exit(1) diff --git a/.assets/multi_selection_custom.gif b/.assets/multi_selection_custom.gif index 211ece4..695a32a 100644 Binary files a/.assets/multi_selection_custom.gif and b/.assets/multi_selection_custom.gif differ diff --git a/.assets/multi_selection_custom.tape b/.assets/multi_selection_custom.tape index 751f4f5..353b108 100644 --- a/.assets/multi_selection_custom.tape +++ b/.assets/multi_selection_custom.tape @@ -8,7 +8,7 @@ Set Height 600 Type@30ms "./examples/multi_selection_custom" Sleep 100ms Enter Sleep 1s -Space +Down Sleep 250ms Down Sleep 250ms Space Sleep 1 diff --git a/examples/multi_selection_custom/main.go b/examples/multi_selection_custom/main.go index fbe1a66..8c54f1a 100644 --- a/examples/multi_selection_custom/main.go +++ b/examples/multi_selection_custom/main.go @@ -30,9 +30,9 @@ func main() { } choices := []lang{ + {name: "Python", icon: "🐍"}, {name: "Go", icon: "🐹"}, {name: "Rust", icon: "🦀"}, - {name: "Python", icon: "🐍"}, {name: "TypeScript", icon: "📘"}, {name: "Haskell", icon: "λ"}, } @@ -44,6 +44,7 @@ func main() { sp.MinSelections = 2 sp.MaxSelections = 4 sp.LoopCursor = true + sp.PreSelected = selection.PreSelect(choices[1]) // Custom styling with emoji icons. sp.UnmarkedChoiceStyle = func(c *selection.Choice[lang]) string { diff --git a/selection/multi_model.go b/selection/multi_model.go index 951f981..23d3281 100644 --- a/selection/multi_model.go +++ b/selection/multi_model.go @@ -81,7 +81,16 @@ func (m *MultiModel[T]) Init() tea.Cmd { } m.filterInput = m.initFilterInput() + m.selectedChoices = make(map[int]bool) + if m.PreSelected != nil { + for _, c := range m.choices { + if m.PreSelected(c) { + m.selectedChoices[c.idx] = true + } + } + } + m.currentChoices, m.availableChoices = m.filteredAndPagedChoices() m.requestedPageSize = m.PageSize diff --git a/selection/multi_model_test.go b/selection/multi_model_test.go index 8f0668a..095013c 100644 --- a/selection/multi_model_test.go +++ b/selection/multi_model_test.go @@ -392,6 +392,33 @@ func TestMultiZeroMinSelections(t *testing.T) { } } +func TestMultiPreSelect(t *testing.T) { + t.Parallel() + + m := selection.NewMultiModel(selection.NewMulti("foo:", []string{"a", "b", "c"})) + m.ColorProfile = termenv.TrueColor + m.PageSize = 2 + m.PreSelected = selection.PreSelect("b", "c") + + test.Run(t, m) + test.AssertGoldenView(t, m, "multi_preselect_pre.golden") + + values := getValues(t, m) + if len(values) != 2 || values[0] != "b" || values[1] != "c" { + t.Fatalf("selected values are not b and c: %#v", values) + } + + test.Update(t, m, keySpace) + + values = getValues(t, m) + if len(values) != 3 || values[0] != "a" || values[1] != "b" || values[2] != "c" { + t.Fatalf("selected values are not a, b and c: %#v", values) + } + + test.Update(t, m, keyEnter) + test.AssertGoldenView(t, m, "multi_preselect_post.golden") +} + var keyEsc = tea.KeyPressMsg{Code: tea.KeyEsc} func getValues[T any](tb testing.TB, m *selection.MultiModel[T]) []T { diff --git a/selection/multi_prompt.go b/selection/multi_prompt.go index dac6dc5..82355f2 100644 --- a/selection/multi_prompt.go +++ b/selection/multi_prompt.go @@ -101,6 +101,11 @@ type MultiSelection[T any] struct { // disabled and FilterPlaceholder does nothing. FilterPlaceholder string + // PreSelected is an optional function that decided whether an option should + // be pre-selected. It can easily be populated from a single choice value or + // a slice of choice values using the selection.PreSelected helper. + PreSelected func(c *Choice[T]) bool + // PageSize is the number of choices that are displayed at once. If // PageSize is smaller than the number of choices, pagination is enabled. // If PageSize is 0, pagination is disabled. Regardless of the value of @@ -286,3 +291,19 @@ func (s *MultiSelection[T]) RunPrompt() ([]T, error) { return m.Values() } + +func PreSelect[T comparable](preSelectedValues ...T) func(c *Choice[T]) bool { + if len(preSelectedValues) == 0 { + return nil + } + + preSelectedMap := map[T]bool{} + + for _, v := range preSelectedValues { + preSelectedMap[v] = true + } + + return func(c *Choice[T]) bool { + return preSelectedMap[c.Value] + } +} diff --git a/selection/testdata/multi_preselect_post.golden b/selection/testdata/multi_preselect_post.golden new file mode 100644 index 0000000..393e5a4 --- /dev/null +++ b/selection/testdata/multi_preselect_post.golden @@ -0,0 +1 @@ +foo: a, b, c diff --git a/selection/testdata/multi_preselect_pre.golden b/selection/testdata/multi_preselect_pre.golden new file mode 100644 index 0000000..1d64b5a --- /dev/null +++ b/selection/testdata/multi_preselect_pre.golden @@ -0,0 +1,5 @@ +foo: +Filter: Type to filter choices  + ▸ ☐ a +⇣ ☑ b + ☑ c