diff options
| author | Paul Buetow <paul@buetow.org> | 2026-06-25 11:18:54 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-06-25 11:18:54 +0300 |
| commit | 51a875e4d665c51e031259d48c9e6c723dafbcef (patch) | |
| tree | b82246cfbbcfbf2a0892562fda0e8c4c4f2809f6 | |
| parent | 541fe26ddb20e00112d0bcbda9249625dfe5d245 (diff) | |
Refactor ultra visibility offset handling (3r0)
| -rw-r--r-- | internal/ui/table_test.go | 80 | ||||
| -rw-r--r-- | internal/ui/ultra.go | 159 |
2 files changed, 171 insertions, 68 deletions
diff --git a/internal/ui/table_test.go b/internal/ui/table_test.go index 7a8a186..3c7cbd7 100644 --- a/internal/ui/table_test.go +++ b/internal/ui/table_test.go @@ -2898,6 +2898,86 @@ func TestUltraEntryResizeAndNavigationBindings(t *testing.T) { } } +func TestUltraEnsureVisibleOffsets(t *testing.T) { + tasks := []task.Task{ + {ID: 1, UUID: "1", Description: "alpha", Status: "pending"}, + {ID: 2, UUID: "2", Description: "beta bravo", Status: "pending"}, + {ID: 3, UUID: "3", Description: "charlie delta", Status: "pending"}, + } + + tests := []struct { + name string + tasks []task.Task + height int + cursor int + offset int + wantCursor int + wantOffset int + }{ + { + name: "empty list clamps cursor and offset", + height: 7, + cursor: 2, + offset: 2, + wantCursor: 0, + wantOffset: 0, + }, + { + name: "cursor above offset scrolls to cursor", + tasks: tasks, + height: 7, + cursor: 0, + offset: 2, + wantCursor: 0, + wantOffset: 0, + }, + { + name: "cursor below visible window scrolls down", + tasks: tasks, + height: 7, + cursor: 2, + offset: 0, + wantCursor: 2, + wantOffset: 1, + }, + { + name: "cursor card taller than viewport scrolls directly to cursor", + tasks: []task.Task{ + {ID: 1, UUID: "1", Description: "alpha", Status: "pending"}, + {ID: 2, UUID: "2", Description: strings.Repeat("long description ", 30), Status: "pending"}, + {ID: 3, UUID: "3", Description: "charlie", Status: "pending"}, + }, + height: 5, + cursor: 1, + offset: 0, + wantCursor: 1, + wantOffset: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m, err := NewWithTaskwarrior(nil, "firefox", &fakeTaskwarrior{tasks: tt.tasks}) + if err != nil { + t.Fatalf("NewWithTaskwarrior: %v", err) + } + m.tbl.SetWidth(60) + m.windowHeight = tt.height + m.ultraCursor = tt.cursor + m.ultraOffset = tt.offset + + m.ultraEnsureVisible() + + if got := m.ultraCursor; got != tt.wantCursor { + t.Fatalf("cursor = %d, want %d", got, tt.wantCursor) + } + if got := m.ultraOffset; got != tt.wantOffset { + t.Fatalf("offset = %d, want %d", got, tt.wantOffset) + } + }) + } +} + func TestUltraBlinkUsesVisibleSelectionAndRendersBlink(t *testing.T) { tmp := t.TempDir() taskPath := setupUltraTaskSet(t, tmp) diff --git a/internal/ui/ultra.go b/internal/ui/ultra.go index 2c06afb..2121aa8 100644 --- a/internal/ui/ultra.go +++ b/internal/ui/ultra.go @@ -187,45 +187,8 @@ func (m *Model) ultraVisibleCount() int { return 0 } - width := m.ultraRenderWidth() - top := m.ultraStatusLine(m.ultraModeStatus(tasks), width) - bottom := m.ultraStatusLine(m.ultraCursorStatus(tasks), width) - _, overlayHeight := m.ultraOverlay() - - budget := m.ultraCardBudget(top, bottom, overlayHeight) - if budget <= 0 { - return 0 - } - - start := m.ultraVisibleStart(len(tasks)) - selected := m.ultraVisibleCursor(tasks) - used := 0 - count := 0 - for i := start; i < len(tasks); i++ { - card := m.renderUltraCard(tasks[i], width, i == selected, m.ultraSearchRegex) - if card == "" { - continue - } - - cardHeight := lipgloss.Height(card) - if count > 0 { - if used+1+cardHeight > budget { - break - } - used++ - } else if cardHeight > budget { - break - } - - if used+cardHeight > budget { - break - } - - used += cardHeight - count++ - } - - return count + budget, heights := m.ultraCardMetrics(tasks) + return ultraVisibleCountFrom(heights, m.ultraVisibleStart(len(tasks)), budget) } func (m *Model) ultraTaskList() []task.Task { @@ -950,6 +913,82 @@ func ultraDueValue(m *Model, due string) string { return ultraOrDash(val) } +func (m *Model) ultraCardMetrics(tasks []task.Task) (int, []int) { + width := m.ultraRenderWidth() + top := m.ultraStatusLine(m.ultraModeStatus(tasks), width) + bottom := m.ultraStatusLine(m.ultraCursorStatus(tasks), width) + _, overlayHeight := m.ultraOverlay() + + budget := m.ultraCardBudget(top, bottom, overlayHeight) + selected := m.ultraVisibleCursor(tasks) + heights := make([]int, len(tasks)) + for i, t := range tasks { + card := m.renderUltraCard(t, width, i == selected, m.ultraSearchRegex) + if card == "" { + continue + } + heights[i] = lipgloss.Height(card) + } + return budget, heights +} + +func ultraVisibleCountFrom(heights []int, start, budget int) int { + if budget <= 0 || start < 0 || start >= len(heights) { + return 0 + } + + used := 0 + count := 0 + for i := start; i < len(heights); i++ { + height := heights[i] + if height <= 0 { + continue + } + + if count > 0 { + if used+1+height > budget { + break + } + used++ + } else if height > budget { + break + } + + if used+height > budget { + break + } + + used += height + count++ + } + return count +} + +func ultraOffsetToShowCursor(heights []int, cursor, budget int) int { + if budget <= 0 || cursor < 0 || cursor >= len(heights) || heights[cursor] > budget { + return cursor + } + + used := heights[cursor] + if used <= 0 { + return cursor + } + + start := cursor + for i := cursor - 1; i >= 0; i-- { + height := heights[i] + if height <= 0 { + continue + } + if used+1+height > budget { + break + } + used += 1 + height + start = i + } + return start +} + func (m *Model) ultraEnsureVisible() { tasks := m.ultraTaskList() if len(tasks) == 0 { @@ -971,38 +1010,22 @@ func (m *Model) ultraEnsureVisible() { m.ultraOffset = len(tasks) - 1 } - for range tasks { - visible := m.ultraVisibleCount() - if visible <= 0 { - if m.ultraOffset == m.ultraCursor { - return - } - m.ultraOffset = m.ultraCursor - continue - } + budget, heights := m.ultraCardMetrics(tasks) + visible := ultraVisibleCountFrom(heights, m.ultraVisibleStart(len(tasks)), budget) + if visible <= 0 { + m.ultraOffset = m.ultraCursor + return + } - start := m.ultraVisibleStart(len(tasks)) - end := start + visible - 1 - if m.ultraCursor < start { - if m.ultraOffset == m.ultraCursor { - return - } - m.ultraOffset = m.ultraCursor - continue - } - if m.ultraCursor > end { - next := m.ultraCursor - visible + 1 - if next < 0 { - next = 0 - } - if next == m.ultraOffset { - return - } - m.ultraOffset = next - continue - } + start := m.ultraVisibleStart(len(tasks)) + end := start + visible - 1 + if m.ultraCursor < start { + m.ultraOffset = m.ultraCursor return } + if m.ultraCursor > end { + m.ultraOffset = ultraOffsetToShowCursor(heights, m.ultraCursor, budget) + } } // handleUltraMode handles keyboard input in ultra mode. |
