From ab2df0ae3356d9a034d432b53f96c8cf0b83bf1b Mon Sep 17 00:00:00 2001 From: spoons-and-mirrors <212802214+spoons-and-mirrors@users.noreply.github.com> Date: Mon, 11 Aug 2025 15:47:51 +0200 Subject: [PATCH] Feat: Implement Wrap-Around Navigation for List Selection (for Models and Tools modal) (#1768) --- packages/tui/internal/components/list/list.go | 29 ++++++----- .../tui/internal/components/list/list_test.go | 51 ++++++++++++++++--- 2 files changed, 62 insertions(+), 18 deletions(-) diff --git a/packages/tui/internal/components/list/list.go b/packages/tui/internal/components/list/list.go index fd2d7d93..a9823d0a 100644 --- a/packages/tui/internal/components/list/list.go +++ b/packages/tui/internal/components/list/list.go @@ -173,7 +173,13 @@ func (c *listComponent[T]) moveUp() { } } - // If no selectable item found above, stay at current position + // If no selectable item found above, wrap to the bottom + for i := len(c.items) - 1; i > c.selectedIdx; i-- { + if c.isSelectable(c.items[i]) { + c.selectedIdx = i + return + } + } } // moveDown moves the selection down, skipping non-selectable items @@ -183,20 +189,19 @@ func (c *listComponent[T]) moveDown() { } originalIdx := c.selectedIdx - for { - if c.selectedIdx < len(c.items)-1 { - c.selectedIdx++ - } else { - break - } - - if c.isSelectable(c.items[c.selectedIdx]) { + // First try moving down from current position + for i := c.selectedIdx + 1; i < len(c.items); i++ { + if c.isSelectable(c.items[i]) { + c.selectedIdx = i return } + } - // Prevent infinite loop - if c.selectedIdx == originalIdx { - break + // If no selectable item found below, wrap to the top + for i := 0; i < originalIdx; i++ { + if c.isSelectable(c.items[i]) { + c.selectedIdx = i + return } } } diff --git a/packages/tui/internal/components/list/list_test.go b/packages/tui/internal/components/list/list_test.go index 663503a4..25cca8cf 100644 --- a/packages/tui/internal/components/list/list_test.go +++ b/packages/tui/internal/components/list/list_test.go @@ -138,15 +138,18 @@ func TestCtrlNavigation(t *testing.T) { func TestNavigationBoundaries(t *testing.T) { list := createTestList() - // Test up arrow at first item (should stay at 0) + // Test up arrow at first item (should wrap to last item) upKey := tea.KeyPressMsg{Code: tea.KeyUp} updatedModel, _ := list.Update(upKey) list = updatedModel.(*listComponent[testItem]) _, idx := list.GetSelectedItem() - if idx != 0 { - t.Errorf("Expected to stay at index 0 when pressing up at first item, got %d", idx) + if idx != 2 { + t.Errorf("Expected to wrap to index 2 when pressing up at first item, got %d", idx) } + // Move to first item + list.SetSelectedIndex(0) + // Move to last item downKey := tea.KeyPressMsg{Code: tea.KeyDown} updatedModel, _ = list.Update(downKey) @@ -158,12 +161,12 @@ func TestNavigationBoundaries(t *testing.T) { t.Errorf("Expected to be at index 2, got %d", idx) } - // Test down arrow at last item (should stay at 2) + // Test down arrow at last item (should wrap to first item) updatedModel, _ = list.Update(downKey) list = updatedModel.(*listComponent[testItem]) _, idx = list.GetSelectedItem() - if idx != 2 { - t.Errorf("Expected to stay at index 2 when pressing down at last item, got %d", idx) + if idx != 0 { + t.Errorf("Expected to wrap to index 0 when pressing down at last item, got %d", idx) } } @@ -208,3 +211,39 @@ func TestEmptyList(t *testing.T) { t.Error("Expected IsEmpty() to return true for empty list") } } + +func TestWrapAroundNavigation(t *testing.T) { + list := createTestList() + + // Start at first item (index 0) + _, idx := list.GetSelectedItem() + if idx != 0 { + t.Errorf("Expected to start at index 0, got %d", idx) + } + + // Press up arrow - should wrap to last item (index 2) + upKey := tea.KeyPressMsg{Code: tea.KeyUp} + updatedModel, _ := list.Update(upKey) + list = updatedModel.(*listComponent[testItem]) + _, idx = list.GetSelectedItem() + if idx != 2 { + t.Errorf("Expected to wrap to index 2 when pressing up from first item, got %d", idx) + } + + // Press down arrow - should wrap to first item (index 0) + downKey := tea.KeyPressMsg{Code: tea.KeyDown} + updatedModel, _ = list.Update(downKey) + list = updatedModel.(*listComponent[testItem]) + _, idx = list.GetSelectedItem() + if idx != 0 { + t.Errorf("Expected to wrap to index 0 when pressing down from last item, got %d", idx) + } + + // Navigate to middle and verify normal navigation still works + updatedModel, _ = list.Update(downKey) + list = updatedModel.(*listComponent[testItem]) + _, idx = list.GetSelectedItem() + if idx != 1 { + t.Errorf("Expected to move to index 1, got %d", idx) + } +}