on main help sign in
chore: small improvements
commit2a13723ac3e953ef873a77ec32d1440d262cc30f authorKujtim Hoxha <kujtimii.h@gmail.com> dateWed 23 Jul 2025, 14:53 (1 year ago) parenta6c4855
4 files changed+60−87
internal/tui/exp/list/list.go+60−31 internal/tui/exp/list/list_test.go−36 internal/tui/exp/list/testdata/TestList/should_go_to_selected_item_and_center.golden−10 internal/tui/exp/list/testdata/TestList/should_go_to_selected_item_and_center_backwards.golden−10
internal/tui/exp/list/list.go+60−31
@@ -56,7 +56,6 @@ const (
5656type renderedItem struct {
5757 id string
5858 view string
59− dirty bool
6059 height int
6160 start int
6261 end int
@@ -84,6 +83,8 @@ type list[T Item] struct {
8483 renderedItems map[string]renderedItem
8584
8685 rendered string
86+
87+ movingByItem bool
8788}
8889
8990type listOption func(*confOptions)
@@ -209,7 +210,9 @@ func (l *list[T]) View() string {
209210 lines := strings.Split(view, "\n")
210211
211212 start, end := l.viewPosition()
212− lines = lines[start : end+1]
213+ viewStart := max(0, start)
214+ viewEnd := min(len(lines), end+1)
215+ lines = lines[viewStart:viewEnd]
213216 return strings.Join(lines, "\n")
214217}
215218
@@ -245,7 +248,13 @@ func (l *list[T]) render() tea.Cmd {
245248 return nil
246249 }
247250 l.setDefaultSelected()
248− focusCmd := l.focusSelectedItem()
251+
252+ var focusChangeCmd tea.Cmd
253+ if l.focused {
254+ focusChangeCmd = l.focusSelectedItem()
255+ } else {
256+ focusChangeCmd = l.blurSelectedItem()
257+ }
249258 // we are not rendering the first time
250259 if l.rendered != "" {
251260 l.rendered = ""
@@ -258,7 +267,7 @@ func (l *list[T]) render() tea.Cmd {
258267 if l.focused {
259268 l.scrollToSelection()
260269 }
261− return focusCmd
270+ return focusChangeCmd
262271 }
263272 finishIndex := l.renderIterator(0, true)
264273 // recalculate for the initial items
@@ -276,9 +285,10 @@ func (l *list[T]) render() tea.Cmd {
276285 if l.focused {
277286 l.scrollToSelection()
278287 }
288+
279289 return renderedMsg{}
280290 }
281− return tea.Batch(focusCmd, renderCmd)
291+ return tea.Batch(focusChangeCmd, renderCmd)
282292}
283293
284294func (l *list[T]) setDefaultSelected() {
@@ -304,11 +314,21 @@ func (l *list[T]) scrollToSelection() {
304314 if rItem.start <= start && rItem.end >= end {
305315 return
306316 }
307− // item already in view do nothing
308− if rItem.start >= start && rItem.start <= end {
309− return
310− } else if rItem.end <= end && rItem.end >= start {
311− return
317+ // if we are moving by item we want to move the offset so that the
318+ // whole item is visible not just portions of it
319+ if l.movingByItem {
320+ if rItem.start >= start && rItem.end <= end {
321+ return
322+ }
323+ defer func() { l.movingByItem = false }()
324+ } else {
325+ // item already in view do nothing
326+ if rItem.start >= start && rItem.start <= end {
327+ return
328+ }
329+ if rItem.end >= start && rItem.end <= end {
330+ return
331+ }
312332 }
313333
314334 if rItem.height >= l.height {
@@ -320,11 +340,22 @@ func (l *list[T]) scrollToSelection() {
320340 return
321341 }
322342
323− itemMiddleStart := rItem.start + rItem.height/2 + 1
324− if l.direction == DirectionForward {
325− l.offset = itemMiddleStart - l.height/2
326− } else {
327− l.offset = max(0, lipgloss.Height(l.rendered)-(itemMiddleStart+l.height/2))
343+ renderedLines := lipgloss.Height(l.rendered) - 1
344+
345+ // If item is above the viewport, make it the first item
346+ if rItem.start < start {
347+ if l.direction == DirectionForward {
348+ l.offset = rItem.start
349+ } else {
350+ l.offset = max(0, renderedLines-rItem.start-l.height+1)
351+ }
352+ } else if rItem.end > end {
353+ // If item is below the viewport, make it the last item
354+ if l.direction == DirectionForward {
355+ l.offset = max(0, rItem.end-l.height+1)
356+ } else {
357+ l.offset = max(0, renderedLines-rItem.end)
358+ }
328359 }
329360}
330361
@@ -446,32 +477,26 @@ func (l *list[T]) focusSelectedItem() tea.Cmd {
446477 if f, ok := any(item).(layout.Focusable); ok {
447478 if item.ID() == l.selectedItem && !f.IsFocused() {
448479 cmds = append(cmds, f.Focus())
449− if cache, ok := l.renderedItems[item.ID()]; ok {
450− cache.dirty = true
451− l.renderedItems[item.ID()] = cache
452− }
480+ delete(l.renderedItems, item.ID())
453481 } else if item.ID() != l.selectedItem && f.IsFocused() {
454482 cmds = append(cmds, f.Blur())
455− if cache, ok := l.renderedItems[item.ID()]; ok {
456− cache.dirty = true
457− l.renderedItems[item.ID()] = cache
458− }
483+ delete(l.renderedItems, item.ID())
459484 }
460485 }
461486 }
462487 return tea.Batch(cmds...)
463488}
464489
465−func (l *list[T]) blurItems() tea.Cmd {
490+func (l *list[T]) blurSelectedItem() tea.Cmd {
491+ if l.selectedItem == "" || l.focused {
492+ return nil
493+ }
466494 var cmds []tea.Cmd
467495 for _, item := range l.items {
468496 if f, ok := any(item).(layout.Focusable); ok {
469497 if item.ID() == l.selectedItem && f.IsFocused() {
470498 cmds = append(cmds, f.Blur())
471− if cache, ok := l.renderedItems[item.ID()]; ok {
472− cache.dirty = true
473− l.renderedItems[item.ID()] = cache
474− }
499+ delete(l.renderedItems, item.ID())
475500 }
476501 }
477502 }
@@ -495,7 +520,7 @@ func (l *list[T]) renderIterator(startInx int, limitHeight bool) int {
495520
496521 item := l.items[inx]
497522 var rItem renderedItem
498− if cache, ok := l.renderedItems[item.ID()]; ok && !cache.dirty {
523+ if cache, ok := l.renderedItems[item.ID()]; ok {
499524 rItem = cache
500525 } else {
501526 rItem = l.renderItem(item)
@@ -534,8 +559,8 @@ func (l *list[T]) AppendItem(T) tea.Cmd {
534559
535560// Blur implements List.
536561func (l *list[T]) Blur() tea.Cmd {
537− cmd := l.blurItems()
538− return tea.Batch(cmd, l.render())
562+ l.focused = false
563+ return l.render()
539564}
540565
541566// DeleteItem implements List.
@@ -644,6 +669,7 @@ func (l *list[T]) SelectItemAbove() tea.Cmd {
644669 }
645670 item := l.items[newIndex]
646671 l.selectedItem = item.ID()
672+ l.movingByItem = true
647673 return l.render()
648674}
649675
@@ -661,6 +687,7 @@ func (l *list[T]) SelectItemBelow() tea.Cmd {
661687 }
662688 item := l.items[newIndex]
663689 l.selectedItem = item.ID()
690+ l.movingByItem = true
664691 return l.render()
665692}
666693
@@ -692,6 +719,8 @@ func (l *list[T]) SetSelected(id string) tea.Cmd {
692719func (l *list[T]) reset() tea.Cmd {
693720 var cmds []tea.Cmd
694721 l.rendered = ""
722+ l.offset = 0
723+ l.selectedItem = ""
695724 l.indexMap = make(map[string]int)
696725 l.renderedItems = make(map[string]renderedItem)
697726 for inx, item := range l.items {
internal/tui/exp/list/list_test.go−36
@@ -205,42 +205,6 @@ func TestList(t *testing.T) {
205205 golden.RequireEqual(t, []byte(l.View()))
206206 })
207207
208− t.Run("should go to selected item and center", func(t *testing.T) {
209− t.Parallel()
210− items := []Item{}
211− for i := range 30 {
212− content := strings.Repeat(fmt.Sprintf("Item %d\n", i), i+1)
213− content = strings.TrimSuffix(content, "\n")
214− item := NewSelectableItem(content)
215− items = append(items, item)
216− }
217− l := New(items, WithDirectionForward(), WithSize(10, 10), WithSelectedItem(items[4].ID())).(*list[Item])
218− execCmd(l, l.Init())
219−
220− // should select the last item
221− assert.Equal(t, items[4].ID(), l.selectedItem)
222−
223− golden.RequireEqual(t, []byte(l.View()))
224− })
225−
226− t.Run("should go to selected item and center backwards", func(t *testing.T) {
227− t.Parallel()
228− items := []Item{}
229− for i := range 30 {
230− content := strings.Repeat(fmt.Sprintf("Item %d\n", i), i+1)
231− content = strings.TrimSuffix(content, "\n")
232− item := NewSelectableItem(content)
233− items = append(items, item)
234− }
235− l := New(items, WithDirectionBackward(), WithSize(10, 10), WithSelectedItem(items[4].ID())).(*list[Item])
236− execCmd(l, l.Init())
237−
238− // should select the last item
239− assert.Equal(t, items[4].ID(), l.selectedItem)
240−
241− golden.RequireEqual(t, []byte(l.View()))
242− })
243−
244208 t.Run("should go to selected item at the beginning", func(t *testing.T) {
245209 t.Parallel()
246210 items := []Item{}
internal/tui/exp/list/testdata/TestList/should_go_to_selected_item_and_center.goldendeleted−10
@@ -1,10 +0,0 @@
1−Item 3
2−Item 3
3−│Item 4
4−│Item 4
5−│Item 4
6−│Item 4
7−│Item 4
8−Item 5
9−Item 5
10−Item 5
internal/tui/exp/list/testdata/TestList/should_go_to_selected_item_and_center_backwards.goldendeleted−10
@@ -1,10 +0,0 @@
1−Item 3
2−Item 3
3−│Item 4
4−│Item 4
5−│Item 4
6−│Item 4
7−│Item 4
8−Item 5
9−Item 5
10−Item 5