package tui import ( "strings" "testing" tea "github.com/charmbracelet/bubbletea" ) func items(names ...string) []SelectItem { var out []SelectItem for _, n := range names { out = append(out, SelectItem{Name: n}) } return out } func recItems(names ...string) []SelectItem { var out []SelectItem for _, n := range names { out = append(out, SelectItem{Name: n, Recommended: true}) } return out } func mixedItems() []SelectItem { return []SelectItem{ {Name: "rec-a", Recommended: true}, {Name: "rec-b", Recommended: true}, {Name: "other-1"}, {Name: "other-2"}, {Name: "other-3"}, {Name: "other-4"}, {Name: "other-5"}, {Name: "other-6"}, {Name: "other-7"}, {Name: "other-8"}, {Name: "other-9"}, {Name: "other-10"}, } } func TestFilteredItems(t *testing.T) { tests := []struct { name string items []SelectItem filter string want []string }{ { name: "no filter returns all", items: items("alpha", "beta", "gamma"), filter: "", want: []string{"alpha", "beta", "gamma"}, }, { name: "filter matches substring", items: items("llama3.2", "qwen3:8b", "llama2"), filter: "llama", want: []string{"llama3.2", "llama2"}, }, { name: "filter is case insensitive", items: items("Qwen3:8b", "llama3.2"), filter: "QWEN", want: []string{"Qwen3:8b"}, }, { name: "no matches", items: items("alpha", "beta"), filter: "zzz", want: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { m := selectorModel{items: tt.items, filter: tt.filter} got := m.filteredItems() var gotNames []string for _, item := range got { gotNames = append(gotNames, item.Name) } if len(gotNames) != len(tt.want) { t.Fatalf("got %v, want %v", gotNames, tt.want) } for i := range tt.want { if gotNames[i] != tt.want[i] { t.Errorf("index %d: got %q, want %q", i, gotNames[i], tt.want[i]) } } }) } } func TestOtherStart(t *testing.T) { tests := []struct { name string items []SelectItem filter string want int }{ { name: "all recommended", items: recItems("a", "b", "c"), want: 3, }, { name: "none recommended", items: items("a", "b"), want: 0, }, { name: "mixed", items: []SelectItem{ {Name: "rec-a", Recommended: true}, {Name: "rec-b", Recommended: true}, {Name: "other-1"}, {Name: "other-2"}, }, want: 2, }, { name: "empty", items: nil, want: 0, }, { name: "filtering returns 0", items: []SelectItem{ {Name: "rec-a", Recommended: true}, {Name: "other-1"}, }, filter: "rec", want: 0, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { m := selectorModel{items: tt.items, filter: tt.filter} if got := m.otherStart(); got != tt.want { t.Errorf("otherStart() = %d, want %d", got, tt.want) } }) } } func TestUpdateScroll(t *testing.T) { tests := []struct { name string cursor int offset int otherStart int filter string wantOffset int }{ { name: "cursor in recommended resets scroll", cursor: 1, offset: 5, otherStart: 3, wantOffset: 0, }, { name: "cursor at start of others", cursor: 2, offset: 0, otherStart: 2, wantOffset: 0, }, { name: "cursor scrolls down in others", cursor: 12, offset: 0, otherStart: 2, wantOffset: 3, // posInOthers=10, maxOthers=8, 10-8+1=3 }, { name: "cursor scrolls up in others", cursor: 4, offset: 5, otherStart: 2, wantOffset: 2, // posInOthers=2 < offset=5 }, { name: "filter mode standard scroll down", cursor: 12, offset: 0, filter: "x", otherStart: 0, wantOffset: 3, // 12 - 10 + 1 = 3 }, { name: "filter mode standard scroll up", cursor: 2, offset: 5, filter: "x", otherStart: 0, wantOffset: 2, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { m := selectorModel{ cursor: tt.cursor, scrollOffset: tt.offset, filter: tt.filter, } m.updateScroll(tt.otherStart) if m.scrollOffset != tt.wantOffset { t.Errorf("scrollOffset = %d, want %d", m.scrollOffset, tt.wantOffset) } }) } } func TestSelectorModelWithCurrent_ScrollsToCurrentInMoreSection(t *testing.T) { m := selectorModelWithCurrent("Pick:", mixedItems(), "other-10") if m.cursor != 11 { t.Fatalf("cursor = %d, want 11", m.cursor) } if m.scrollOffset == 0 { t.Fatal("scrollOffset should move to reveal current item in More section") } content := m.renderContent() if !strings.Contains(content, "▸ other-10") { t.Fatalf("expected current item to be visible and highlighted\n%s", content) } } func TestSelectorModelWithCurrent_HighlightsExactLocalWhenCloudVariantExists(t *testing.T) { m := selectorModelWithCurrent("Pick:", []SelectItem{ {Name: "qwen3.5:cloud", Recommended: true}, {Name: "qwen3.5", Recommended: true}, }, "qwen3.5") if m.cursor != 1 { t.Fatalf("cursor = %d, want 1", m.cursor) } content := m.renderContent() if !strings.Contains(content, "▸ qwen3.5") { t.Fatalf("expected local qwen3.5 to be highlighted\n%s", content) } if strings.Contains(content, "▸ qwen3.5:cloud") { t.Fatalf("did not expect cloud qwen3.5:cloud to be highlighted\n%s", content) } } func TestRenderContent_SectionHeaders(t *testing.T) { m := selectorModel{ title: "Pick:", items: []SelectItem{ {Name: "rec-a", Recommended: true}, {Name: "other-1"}, }, } content := m.renderContent() if !strings.Contains(content, "Recommended") { t.Error("should contain 'Recommended' header") } if !strings.Contains(content, "More") { t.Error("should contain 'More' header") } } func TestRenderContent_FilteredHeader(t *testing.T) { m := selectorModel{ title: "Pick:", items: items("alpha", "beta", "alphabet"), filter: "alpha", } content := m.renderContent() if !strings.Contains(content, "Top Results") { t.Error("filtered view should contain 'Top Results' header") } if strings.Contains(content, "Recommended") { t.Error("filtered view should not contain 'Recommended' header") } } func TestRenderContent_NoMatches(t *testing.T) { m := selectorModel{ title: "Pick:", items: items("alpha"), filter: "zzz", } content := m.renderContent() if !strings.Contains(content, "(no matches)") { t.Error("should show '(no matches)' when filter has no results") } } func TestRenderContent_SelectedItemIndicator(t *testing.T) { m := selectorModel{ title: "Pick:", items: items("alpha", "beta"), cursor: 0, } content := m.renderContent() if !strings.Contains(content, "▸") { t.Error("selected item should have ▸ indicator") } } func TestRenderContent_Description(t *testing.T) { m := selectorModel{ title: "Pick:", items: []SelectItem{ {Name: "alpha", Description: "the first letter"}, }, } content := m.renderContent() if !strings.Contains(content, "the first letter") { t.Error("should render item description") } } func TestRenderContent_PinnedRecommended(t *testing.T) { m := selectorModel{ title: "Pick:", items: mixedItems(), // cursor deep in "More" section cursor: 8, scrollOffset: 3, } content := m.renderContent() // Recommended items should always be visible (pinned) if !strings.Contains(content, "rec-a") { t.Error("recommended items should always be rendered (pinned)") } if !strings.Contains(content, "rec-b") { t.Error("recommended items should always be rendered (pinned)") } } func TestRenderContent_MoreOverflowIndicator(t *testing.T) { m := selectorModel{ title: "Pick:", items: mixedItems(), // 2 rec + 10 other = 12 total, maxSelectorItems=10 } content := m.renderContent() if !strings.Contains(content, "... and") { t.Error("should show overflow indicator when more items than visible") } } func TestUpdateNavigation_CursorBounds(t *testing.T) { m := selectorModel{ items: items("a", "b", "c"), cursor: 0, } // Up at top stays at 0 m.updateNavigation(keyMsg(KeyUp)) if m.cursor != 0 { t.Errorf("cursor should stay at 0 when pressing up at top, got %d", m.cursor) } // Down moves to 1 m.updateNavigation(keyMsg(KeyDown)) if m.cursor != 1 { t.Errorf("cursor should be 1 after down, got %d", m.cursor) } // Down to end m.updateNavigation(keyMsg(KeyDown)) m.updateNavigation(keyMsg(KeyDown)) if m.cursor != 2 { t.Errorf("cursor should be 2 at bottom, got %d", m.cursor) } } func TestUpdateNavigation_FilterResetsState(t *testing.T) { m := selectorModel{ items: items("alpha", "beta"), cursor: 1, scrollOffset: 5, } m.updateNavigation(runeMsg('x')) if m.filter != "x" { t.Errorf("filter should be 'x', got %q", m.filter) } if m.cursor != 0 { t.Errorf("cursor should reset to 0 on filter, got %d", m.cursor) } if m.scrollOffset != 0 { t.Errorf("scrollOffset should reset to 0 on filter, got %d", m.scrollOffset) } } func TestUpdateNavigation_Backspace(t *testing.T) { m := selectorModel{ items: items("alpha"), filter: "abc", cursor: 1, } m.updateNavigation(keyMsg(KeyBackspace)) if m.filter != "ab" { t.Errorf("filter should be 'ab' after backspace, got %q", m.filter) } if m.cursor != 0 { t.Errorf("cursor should reset to 0 on backspace, got %d", m.cursor) } } // --- cursorForCurrent --- func TestCursorForCurrent(t *testing.T) { testItems := []SelectItem{ {Name: "llama3.2", Recommended: true}, {Name: "qwen3:8b", Recommended: true}, {Name: "gemma3:latest"}, {Name: "deepseek-r1"}, {Name: "glm-5:cloud"}, } tests := []struct { name string current string want int }{ {"empty current", "", 0}, {"exact match", "qwen3:8b", 1}, {"no match returns 0", "nonexistent", 0}, {"bare name matches with :latest suffix", "gemma3", 2}, {"full tag matches bare item", "llama3.2:latest", 0}, {"cloud model exact match", "glm-5:cloud", 4}, {"cloud model bare name", "glm-5", 4}, {"recommended item exact match", "llama3.2", 0}, {"recommended item with tag", "qwen3", 1}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := cursorForCurrent(testItems, tt.current); got != tt.want { t.Errorf("cursorForCurrent(%q) = %d, want %d", tt.current, got, tt.want) } }) } } func TestCursorForCurrent_PrefersExactLocalOverCloudPrefix(t *testing.T) { testItems := []SelectItem{ {Name: "qwen3.5:cloud", Recommended: true}, {Name: "qwen3.5", Recommended: true}, } if got := cursorForCurrent(testItems, "qwen3.5"); got != 1 { t.Errorf("cursorForCurrent(%q) = %d, want %d", "qwen3.5", got, 1) } } func TestCursorForCurrent_PrefersExactCloudOverLocalPrefix(t *testing.T) { testItems := []SelectItem{ {Name: "qwen3.5", Recommended: true}, {Name: "qwen3.5:cloud", Recommended: true}, } if got := cursorForCurrent(testItems, "qwen3.5:cloud"); got != 1 { t.Errorf("cursorForCurrent(%q) = %d, want %d", "qwen3.5:cloud", got, 1) } } // --- ReorderItems --- func TestReorderItems(t *testing.T) { input := []SelectItem{ {Name: "local-1"}, {Name: "rec-a", Recommended: true}, {Name: "local-2"}, {Name: "rec-b", Recommended: true}, } got := ReorderItems(input) want := []string{"rec-a", "rec-b", "local-1", "local-2"} for i, item := range got { if item.Name != want[i] { t.Errorf("index %d: got %q, want %q", i, item.Name, want[i]) } } } func TestReorderItems_AllRecommended(t *testing.T) { input := recItems("a", "b", "c") got := ReorderItems(input) if len(got) != 3 { t.Fatalf("expected 3 items, got %d", len(got)) } for i, item := range got { if item.Name != input[i].Name { t.Errorf("order should be preserved, index %d: got %q, want %q", i, item.Name, input[i].Name) } } } func TestReorderItems_NoneRecommended(t *testing.T) { input := items("x", "y") got := ReorderItems(input) if len(got) != 2 || got[0].Name != "x" || got[1].Name != "y" { t.Errorf("order should be preserved, got %v", got) } } // --- Multi-select otherStart --- func TestMultiOtherStart(t *testing.T) { tests := []struct { name string items []SelectItem filter string want int }{ {"all recommended", recItems("a", "b"), "", 2}, {"none recommended", items("a", "b"), "", 0}, {"mixed", mixedItems(), "", 2}, {"with filter returns 0", mixedItems(), "other", 0}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { m := newMultiSelectorModel("test", tt.items, nil) m.filter = tt.filter if got := m.otherStart(); got != tt.want { t.Errorf("otherStart() = %d, want %d", got, tt.want) } }) } } // --- Multi-select updateScroll --- func TestMultiUpdateScroll(t *testing.T) { tests := []struct { name string cursor int offset int otherStart int wantOffset int }{ {"cursor in recommended resets scroll", 1, 5, 3, 0}, {"cursor at start of others", 2, 0, 2, 0}, {"cursor scrolls down in others", 12, 0, 2, 3}, {"cursor scrolls up in others", 4, 5, 2, 2}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { m := newMultiSelectorModel("test", nil, nil) m.cursor = tt.cursor m.scrollOffset = tt.offset m.updateScroll(tt.otherStart) if m.scrollOffset != tt.wantOffset { t.Errorf("scrollOffset = %d, want %d", m.scrollOffset, tt.wantOffset) } }) } } // --- Multi-select View section headers --- func TestMultiView_SectionHeaders(t *testing.T) { m := newMultiSelectorModel("Pick:", []SelectItem{ {Name: "rec-a", Recommended: true}, {Name: "other-1"}, }, nil) content := m.View() if !strings.Contains(content, "Recommended") { t.Error("should contain 'Recommended' header") } if !strings.Contains(content, "More") { t.Error("should contain 'More' header") } } func TestMultiView_CursorIndicator(t *testing.T) { m := newMultiSelectorModel("Pick:", items("a", "b"), nil) m.cursor = 0 content := m.View() if !strings.Contains(content, "▸") { t.Error("should show ▸ cursor indicator") } } func TestMultiView_CheckedItemShowsX(t *testing.T) { m := newMultiSelectorModel("Pick:", items("a", "b"), []string{"a"}) m.multi = true content := m.View() if !strings.Contains(content, "[x]") { t.Error("checked item should show [x]") } if !strings.Contains(content, "[ ]") { t.Error("unchecked item should show [ ]") } } func TestMultiView_DefaultTag(t *testing.T) { m := newMultiSelectorModel("Pick:", items("a", "b", "c"), []string{"a", "b"}) m.multi = true content := m.View() if !strings.Contains(content, "(default)") { t.Error("should have (default) tag") } // preChecked[0] ("a") should be the default (last in checkOrder) aIdx := strings.Index(content, "a") defaultIdx := strings.Index(content, "(default)") if defaultIdx < aIdx { t.Error("(default) tag should appear after 'a' (the current default)") } } func TestMultiView_PinnedRecommended(t *testing.T) { m := newMultiSelectorModel("Pick:", mixedItems(), nil) m.cursor = 8 m.scrollOffset = 3 content := m.View() if !strings.Contains(content, "rec-a") { t.Error("recommended items should always be visible (pinned)") } if !strings.Contains(content, "rec-b") { t.Error("recommended items should always be visible (pinned)") } } func TestMultiView_OverflowIndicator(t *testing.T) { m := newMultiSelectorModel("Pick:", mixedItems(), nil) content := m.View() if !strings.Contains(content, "... and") { t.Error("should show overflow indicator when more items than visible") } } // --- Multi-select space toggle (including KeyRunes fallback for Windows PowerShell) --- func TestMultiUpdate_SpaceTogglesItem(t *testing.T) { m := newMultiSelectorModel("Pick:", items("a", "b", "c"), nil) m.multi = true m.cursor = 1 // Simulate space delivered as tea.KeySpace updated, _ := m.Update(tea.KeyMsg{Type: tea.KeySpace}) m = updated.(multiSelectorModel) if !m.checked[1] { t.Error("space (KeySpace) should toggle the item at cursor") } if m.filter != "" { t.Error("space should not modify filter") } } func TestMultiUpdate_SpaceRuneTogglesItem(t *testing.T) { m := newMultiSelectorModel("Pick:", items("a", "b", "c"), nil) m.multi = true m.cursor = 1 // Simulate space delivered as tea.KeyRunes (Windows PowerShell behavior) updated, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{' '}}) m = updated.(multiSelectorModel) if !m.checked[1] { t.Error("space (KeyRunes) should toggle the item at cursor") } if m.filter != "" { t.Error("space rune should not be added to filter") } if m.cursor != 1 { t.Errorf("cursor should stay at 1, got %d", m.cursor) } } // --- Single-add mode --- func TestMulti_StartsInSingleMode(t *testing.T) { m := newMultiSelectorModel("Pick:", items("a", "b"), nil) if m.multi { t.Error("should start in single mode (multi=false)") } } func TestMulti_SingleModeNoCheckboxes(t *testing.T) { m := newMultiSelectorModel("Pick:", items("a", "b"), nil) content := m.View() if strings.Contains(content, "[x]") || strings.Contains(content, "[ ]") { t.Error("single mode should not show checkboxes") } if !strings.Contains(content, "▸") { t.Error("single mode should show cursor indicator") } } func TestMulti_SingleModeEnterPicksItem(t *testing.T) { m := newMultiSelectorModel("Pick:", items("a", "b", "c"), nil) m.cursor = 1 updated, _ := m.Update(tea.KeyMsg{Type: tea.KeyEnter}) m = updated.(multiSelectorModel) if m.singleAdd != "b" { t.Errorf("enter in single mode should pick cursor item, got %q", m.singleAdd) } if !m.confirmed { t.Error("should set confirmed") } } func TestMulti_SingleModeSpaceIsNoop(t *testing.T) { m := newMultiSelectorModel("Pick:", items("a", "b"), nil) m.cursor = 0 updated, _ := m.Update(tea.KeyMsg{Type: tea.KeySpace}) m = updated.(multiSelectorModel) if len(m.checked) != 0 { t.Error("space in single mode should not toggle items") } } func TestMulti_SingleModeSpaceRuneIsNoop(t *testing.T) { m := newMultiSelectorModel("Pick:", items("a", "b"), nil) m.cursor = 0 updated, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{' '}}) m = updated.(multiSelectorModel) if len(m.checked) != 0 { t.Error("space rune in single mode should not toggle items") } if m.filter != "" { t.Error("space rune in single mode should not add to filter") } } func TestMulti_TabTogglesMode(t *testing.T) { m := newMultiSelectorModel("Pick:", items("a", "b"), nil) if m.multi { t.Fatal("should start in single mode") } updated, _ := m.Update(tea.KeyMsg{Type: tea.KeyTab}) m = updated.(multiSelectorModel) if !m.multi { t.Error("tab should switch to multi mode") } updated, _ = m.Update(tea.KeyMsg{Type: tea.KeyTab}) m = updated.(multiSelectorModel) if m.multi { t.Error("tab should switch back to single mode") } } func TestMulti_SingleModeHelpText(t *testing.T) { m := newMultiSelectorModel("Pick:", items("a"), nil) content := m.View() if !strings.Contains(content, "tab add multiple") { t.Error("single mode should show 'tab add multiple' in help") } } func TestMulti_MultiModeHelpText(t *testing.T) { m := newMultiSelectorModel("Pick:", items("a"), nil) m.multi = true content := m.View() if !strings.Contains(content, "tab select single") { t.Error("multi mode should show 'tab select single' in help") } if !strings.Contains(content, "← back") { t.Error("multi mode should show '← back' in help") } } // --- preChecked initialization order --- func TestMulti_PreCheckedDefaultIsLast(t *testing.T) { // preChecked[0] ("a") is the current default and should end up // last in checkOrder so it gets the (default) tag. m := newMultiSelectorModel("Pick:", items("a", "b", "c"), []string{"a", "b", "c"}) if len(m.checkOrder) != 3 { t.Fatalf("expected 3 in checkOrder, got %d", len(m.checkOrder)) } lastIdx := m.checkOrder[len(m.checkOrder)-1] if m.items[lastIdx].Name != "a" { t.Errorf("preChecked[0] should be last in checkOrder, got %q", m.items[lastIdx].Name) } } func TestMulti_CursorOnDefaultModel(t *testing.T) { // preChecked[0] ("b") is the default; cursor should start on it m := newMultiSelectorModel("Pick:", items("a", "b", "c"), []string{"b", "c"}) if m.cursor != 1 { t.Errorf("cursor should be on preChecked[0] ('b') at index 1, got %d", m.cursor) } } // --- Multi-mode last-checked is default --- func TestMulti_LastCheckedIsDefault(t *testing.T) { m := newMultiSelectorModel("Pick:", items("alpha", "beta", "gamma"), nil) m.multi = true // Check "alpha" then "gamma" m.cursor = 0 m.toggleItem() m.cursor = 2 m.toggleItem() // Last checked ("gamma") should be at the end of checkOrder lastIdx := m.checkOrder[len(m.checkOrder)-1] if m.items[lastIdx].Name != "gamma" { t.Errorf("last checked should be 'gamma', got %q", m.items[lastIdx].Name) } // The (default) tag renders based on checkOrder[len-1] content := m.View() if !strings.Contains(content, "(default)") { t.Fatal("should show (default) tag") } // "alpha" line should NOT have the default tag for _, line := range strings.Split(content, "\n") { if strings.Contains(line, "alpha") && strings.Contains(line, "(default)") { t.Error("'alpha' (first checked) should not have (default) tag") } } } func TestMulti_UncheckingDefaultFallsBackToNearestCheckedAbove(t *testing.T) { // Default is "b", and checked models are "a", "b", "c". // Unticking default should make "a" (the nearest checked item above) default. m := newMultiSelectorModel("Pick:", items("a", "b", "c"), []string{"b", "c", "a"}) m.multi = true m.cursor = 1 // "b" m.toggleItem() lastIdx := m.checkOrder[len(m.checkOrder)-1] if m.items[lastIdx].Name != "a" { t.Fatalf("expected default to fall back to 'a', got %q", m.items[lastIdx].Name) } } func TestMulti_UncheckingTopDefaultFallsBackToNearestCheckedBelow(t *testing.T) { // Default is top item "a". With no checked item above, fallback should pick // the nearest checked item below ("b"). m := newMultiSelectorModel("Pick:", items("a", "b", "c"), []string{"a", "c", "b"}) m.multi = true m.cursor = 0 // "a" m.toggleItem() lastIdx := m.checkOrder[len(m.checkOrder)-1] if m.items[lastIdx].Name != "b" { t.Fatalf("expected default to fall back to 'b', got %q", m.items[lastIdx].Name) } } // --- Left arrow back navigation --- func TestSelectorLeftArrowCancelsWhenNoFilter(t *testing.T) { m := selectorModelWithCurrent("Pick:", items("a", "b", "c"), "") updated, _ := m.Update(tea.KeyMsg{Type: tea.KeyLeft}) got := updated.(selectorModel) if !got.cancelled { t.Error("left arrow with empty filter should cancel (go back)") } } func TestSelectorLeftArrowCancelsWhenFiltering(t *testing.T) { m := selectorModelWithCurrent("Pick:", items("a", "b", "c"), "") m.filter = "a" updated, _ := m.Update(tea.KeyMsg{Type: tea.KeyLeft}) got := updated.(selectorModel) if !got.cancelled { t.Error("left arrow with active filter should still cancel (go back)") } } func TestMultiSelectorLeftArrowCancelsWhenNoFilter(t *testing.T) { m := newMultiSelectorModel("Pick:", items("a", "b", "c"), nil) updated, _ := m.Update(tea.KeyMsg{Type: tea.KeyLeft}) got := updated.(multiSelectorModel) if !got.cancelled { t.Error("left arrow with empty filter should cancel (go back)") } } func TestMultiSelectorLeftArrowCancelsWhenFiltering(t *testing.T) { m := newMultiSelectorModel("Pick:", items("a", "b", "c"), nil) m.filter = "a" updated, _ := m.Update(tea.KeyMsg{Type: tea.KeyLeft}) got := updated.(multiSelectorModel) if !got.cancelled { t.Error("left arrow with active filter should still cancel (go back)") } } // Key message helpers for testing type keyType = int const ( KeyUp keyType = iota KeyDown keyType = iota KeyBackspace keyType = iota ) func keyMsg(k keyType) tea.KeyMsg { switch k { case KeyUp: return tea.KeyMsg{Type: tea.KeyUp} case KeyDown: return tea.KeyMsg{Type: tea.KeyDown} case KeyBackspace: return tea.KeyMsg{Type: tea.KeyBackspace} default: return tea.KeyMsg{} } } func runeMsg(r rune) tea.KeyMsg { return tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{r}} }