summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-06-25 11:18:54 +0300
committerPaul Buetow <paul@buetow.org>2026-06-25 11:18:54 +0300
commit51a875e4d665c51e031259d48c9e6c723dafbcef (patch)
treeb82246cfbbcfbf2a0892562fda0e8c4c4f2809f6
parent541fe26ddb20e00112d0bcbda9249625dfe5d245 (diff)
Refactor ultra visibility offset handling (3r0)
-rw-r--r--internal/ui/table_test.go80
-rw-r--r--internal/ui/ultra.go159
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.