on main help sign in
refactor(list): remove item IDs for simpler API
17de9c7Ayman Bagabas
internal/ui/list/example_test.go+14−15
@@ -12,15 +12,15 @@ import (
12func Example_basic() {
12func Example_basic() {
13 // Create some items
13 // Create some items
14 items := []list.Item{
14 items := []list.Item{
15− list.NewStringItem("1", "First item"),
15+ list.NewStringItem("First item"),
16− list.NewStringItem("2", "Second item"),
16+ list.NewStringItem("Second item"),
17− list.NewStringItem("3", "Third item"),
17+ list.NewStringItem("Third item"),
18 }
18 }
19
19
20 // Create a list with options
20 // Create a list with options
21 l := list.New(items...)
21 l := list.New(items...)
22 l.SetSize(80, 10)
22 l.SetSize(80, 10)
23− l.SetSelectedIndex(0)
23+ l.SetSelected(0)
24 if true {
24 if true {
25 l.Focus()
25 l.Focus()
26 }
26 }
@@ -109,7 +109,7 @@ func Example_focusable() {
109 // Create list with first item selected and focused
109 // Create list with first item selected and focused
110 l := list.New(items...)
110 l := list.New(items...)
111 l.SetSize(80, 20)
111 l.SetSize(80, 20)
112− l.SetSelectedIndex(0)
112+ l.SetSelected(0)
113 if true {
113 if true {
114 l.Focus()
114 l.Focus()
115 }
115 }
@@ -127,8 +127,8 @@ func Example_focusable() {
127// Example demonstrates dynamic item updates.
127// Example demonstrates dynamic item updates.
128func Example_dynamicUpdates() {
128func Example_dynamicUpdates() {
129 items := []list.Item{
129 items := []list.Item{
130− list.NewStringItem("1", "Item 1"),
130+ list.NewStringItem("Item 1"),
131− list.NewStringItem("2", "Item 2"),
131+ list.NewStringItem("Item 2"),
132 }
132 }
133
133
134 l := list.New(items...)
134 l := list.New(items...)
@@ -140,13 +140,13 @@ func Example_dynamicUpdates() {
140 l.Draw(&screen, area)
140 l.Draw(&screen, area)
141
141
142 // Update an item
142 // Update an item
143− l.UpdateItem("2", list.NewStringItem("2", "Updated Item 2"))
143+ l.UpdateItem(2, list.NewStringItem("Updated Item 2"))
144
144
145 // Draw again - only changed item is re-rendered
145 // Draw again - only changed item is re-rendered
146 l.Draw(&screen, area)
146 l.Draw(&screen, area)
147
147
148 // Append a new item
148 // Append a new item
149− l.AppendItem(list.NewStringItem("3", "New Item 3"))
149+ l.AppendItem(list.NewStringItem("New Item 3"))
150
150
151 // Draw again - master buffer grows efficiently
151 // Draw again - master buffer grows efficiently
152 l.Draw(&screen, area)
152 l.Draw(&screen, area)
@@ -161,7 +161,6 @@ func Example_scrolling() {
161 items := make([]list.Item, 100)
161 items := make([]list.Item, 100)
162 for i := range items {
162 for i := range items {
163 items[i] = list.NewStringItem(
163 items[i] = list.NewStringItem(
164− fmt.Sprintf("%d", i),
165 fmt.Sprintf("Item %d", i),
164 fmt.Sprintf("Item %d", i),
166 )
165 )
167 }
166 }
@@ -169,7 +168,7 @@ func Example_scrolling() {
169 // Create list with small viewport
168 // Create list with small viewport
170 l := list.New(items...)
169 l := list.New(items...)
171 l.SetSize(80, 10)
170 l.SetSize(80, 10)
172− l.SetSelectedIndex(0)
171+ l.SetSelected(0)
173
172
174 // Draw initial view (shows items 0-9)
173 // Draw initial view (shows items 0-9)
175 screen := uv.NewScreenBuffer(80, 10)
174 screen := uv.NewScreenBuffer(80, 10)
@@ -181,7 +180,7 @@ func Example_scrolling() {
181 l.Draw(&screen, area) // Now shows items 5-14
180 l.Draw(&screen, area) // Now shows items 5-14
182
181
183 // Jump to specific item
182 // Jump to specific item
184− l.ScrollToItem("50")
183+ l.ScrollToItem(50)
185 l.Draw(&screen, area) // Now shows item 50 and neighbors
184 l.Draw(&screen, area) // Now shows item 50 and neighbors
186
185
187 // Scroll to bottom
186 // Scroll to bottom
@@ -258,9 +257,9 @@ func Example_variableHeights() {
258func Example_markdown() {
257func Example_markdown() {
259 // Create markdown items
258 // Create markdown items
260 items := []list.Item{
259 items := []list.Item{
261− list.NewMarkdownItem("1", "# Welcome\n\nThis is a **markdown** item."),
260+ list.NewMarkdownItem("# Welcome\n\nThis is a **markdown** item."),
262− list.NewMarkdownItem("2", "## Features\n\n- Supports **bold**\n- Supports *italic*\n- Supports `code`"),
261+ list.NewMarkdownItem("## Features\n\n- Supports **bold**\n- Supports *italic*\n- Supports `code`"),
263− list.NewMarkdownItem("3", "### Code Block\n\n```go\nfunc main() {\n fmt.Println(\"Hello\")\n}\n```"),
262+ list.NewMarkdownItem("### Code Block\n\n```go\nfunc main() {\n fmt.Println(\"Hello\")\n}\n```"),
264 }
263 }
265
264
266 // Create list
265 // Create list
internal/ui/list/item.go+5−30
@@ -60,9 +60,6 @@ func toUVStyle(lgStyle lipgloss.Style) uv.Style {
60type Item interface {
60type Item interface {
61 uv.Drawable
61 uv.Drawable
62
62
63− // ID returns unique identifier for this item.
64− ID() string
65−
66 // Height returns the item's height in lines for the given width.
63 // Height returns the item's height in lines for the given width.
67 // This allows items to calculate height based on text wrapping and available space.
64 // This allows items to calculate height based on text wrapping and available space.
68 Height(width int) int
65 Height(width int) int
@@ -272,7 +269,6 @@ func (b *BaseHighlightable) ApplyHighlight(buf *uv.ScreenBuffer, width, height i
272type StringItem struct {
269type StringItem struct {
273 BaseFocusable
270 BaseFocusable
274 BaseHighlightable
271 BaseHighlightable
275− id string
276 content string // Raw content string (may contain ANSI styles)
272 content string // Raw content string (may contain ANSI styles)
277 wrap bool // Whether to wrap text
273 wrap bool // Whether to wrap text
278
274
@@ -305,9 +301,8 @@ func LipglossStyleToCellStyler(lgStyle lipgloss.Style) CellStyler {
305}
301}
306
302
307// NewStringItem creates a new string item with the given ID and content.
303// NewStringItem creates a new string item with the given ID and content.
308−func NewStringItem(id, content string) *StringItem {
304+func NewStringItem(content string) *StringItem {
309 s := &StringItem{
305 s := &StringItem{
310− id: id,
311 content: content,
306 content: content,
312 wrap: false,
307 wrap: false,
313 cache: make(map[int]string),
308 cache: make(map[int]string),
@@ -317,9 +312,8 @@ func NewStringItem(id, content string) *StringItem {
317}
312}
318
313
319// NewWrappingStringItem creates a new string item that wraps text to fit width.
314// NewWrappingStringItem creates a new string item that wraps text to fit width.
320−func NewWrappingStringItem(id, content string) *StringItem {
315+func NewWrappingStringItem(content string) *StringItem {
321 s := &StringItem{
316 s := &StringItem{
322− id: id,
323 content: content,
317 content: content,
324 wrap: true,
318 wrap: true,
325 cache: make(map[int]string),
319 cache: make(map[int]string),
@@ -335,11 +329,6 @@ func (s *StringItem) WithFocusStyles(focusStyle, blurStyle *lipgloss.Style) *Str
335 return s
329 return s
336}
330}
337
331
338−// ID implements Item.
339−func (s *StringItem) ID() string {
340− return s.id
341−}
342−
343// Height implements Item.
332// Height implements Item.
344func (s *StringItem) Height(width int) int {
333func (s *StringItem) Height(width int) int {
345 // Calculate content width if we have styles
334 // Calculate content width if we have styles
@@ -423,7 +412,6 @@ func (s *StringItem) SetHighlight(startLine, startCol, endLine, endCol int) {
423type MarkdownItem struct {
412type MarkdownItem struct {
424 BaseFocusable
413 BaseFocusable
425 BaseHighlightable
414 BaseHighlightable
426− id string
427 markdown string // Raw markdown content
415 markdown string // Raw markdown content
428 styleConfig *ansi.StyleConfig // Optional style configuration
416 styleConfig *ansi.StyleConfig // Optional style configuration
429 maxWidth int // Maximum wrap width (default 120)
417 maxWidth int // Maximum wrap width (default 120)
@@ -438,9 +426,8 @@ const DefaultMarkdownMaxWidth = 120
438
426
439// NewMarkdownItem creates a new markdown item with the given ID and markdown content.
427// NewMarkdownItem creates a new markdown item with the given ID and markdown content.
440// If focusStyle and blurStyle are both non-nil, the item will implement Focusable.
428// If focusStyle and blurStyle are both non-nil, the item will implement Focusable.
441−func NewMarkdownItem(id, markdown string) *MarkdownItem {
429+func NewMarkdownItem(markdown string) *MarkdownItem {
442 m := &MarkdownItem{
430 m := &MarkdownItem{
443− id: id,
444 markdown: markdown,
431 markdown: markdown,
445 maxWidth: DefaultMarkdownMaxWidth,
432 maxWidth: DefaultMarkdownMaxWidth,
446 cache: make(map[int]string),
433 cache: make(map[int]string),
@@ -468,11 +455,6 @@ func (m *MarkdownItem) WithFocusStyles(focusStyle, blurStyle *lipgloss.Style) *M
468 return m
455 return m
469}
456}
470
457
471−// ID implements Item.
472−func (m *MarkdownItem) ID() string {
473− return m.id
474−}
475−
476// Height implements Item.
458// Height implements Item.
477func (m *MarkdownItem) Height(width int) int {
459func (m *MarkdownItem) Height(width int) int {
478 // Render the markdown to get its height
460 // Render the markdown to get its height
@@ -563,30 +545,23 @@ func (m *MarkdownItem) SetHighlight(startLine, startCol, endLine, endCol int) {
563}
545}
564
546
565// Gap is a 1-line spacer item used to add gaps between items.
547// Gap is a 1-line spacer item used to add gaps between items.
566−var Gap = NewSpacerItem("spacer-gap", 1)
548+var Gap = NewSpacerItem(1)
567
549
568// SpacerItem is an empty item that takes up vertical space.
550// SpacerItem is an empty item that takes up vertical space.
569// Useful for adding gaps between items in a list.
551// Useful for adding gaps between items in a list.
570type SpacerItem struct {
552type SpacerItem struct {
571− id string
572 height int
553 height int
573}
554}
574
555
575var _ Item = (*SpacerItem)(nil)
556var _ Item = (*SpacerItem)(nil)
576
557
577// NewSpacerItem creates a new spacer item with the given ID and height in lines.
558// NewSpacerItem creates a new spacer item with the given ID and height in lines.
578−func NewSpacerItem(id string, height int) *SpacerItem {
559+func NewSpacerItem(height int) *SpacerItem {
579 return &SpacerItem{
560 return &SpacerItem{
580− id: id,
581 height: height,
561 height: height,
582 }
562 }
583}
563}
584
564
585−// ID implements Item.
586−func (s *SpacerItem) ID() string {
587− return s.id
588−}
589−
590// Height implements Item.
565// Height implements Item.
591func (s *SpacerItem) Height(width int) int {
566func (s *SpacerItem) Height(width int) int {
592 return s.height
567 return s.height
internal/ui/list/item_test.go+51−60
@@ -10,9 +10,9 @@ import (
10
10
11func TestRenderHelper(t *testing.T) {
11func TestRenderHelper(t *testing.T) {
12 items := []Item{
12 items := []Item{
13− NewStringItem("1", "Item 1"),
13+ NewStringItem("Item 1"),
14− NewStringItem("2", "Item 2"),
14+ NewStringItem("Item 2"),
15− NewStringItem("3", "Item 3"),
15+ NewStringItem("Item 3"),
16 }
16 }
17
17
18 l := New(items...)
18 l := New(items...)
@@ -39,11 +39,11 @@ func TestRenderHelper(t *testing.T) {
39
39
40func TestRenderWithScrolling(t *testing.T) {
40func TestRenderWithScrolling(t *testing.T) {
41 items := []Item{
41 items := []Item{
42− NewStringItem("1", "Item 1"),
42+ NewStringItem("Item 1"),
43− NewStringItem("2", "Item 2"),
43+ NewStringItem("Item 2"),
44− NewStringItem("3", "Item 3"),
44+ NewStringItem("Item 3"),
45− NewStringItem("4", "Item 4"),
45+ NewStringItem("Item 4"),
46− NewStringItem("5", "Item 5"),
46+ NewStringItem("Item 5"),
47 }
47 }
48
48
49 l := New(items...)
49 l := New(items...)
@@ -89,8 +89,8 @@ func TestRenderEmptyList(t *testing.T) {
89
89
90func TestRenderVsDrawConsistency(t *testing.T) {
90func TestRenderVsDrawConsistency(t *testing.T) {
91 items := []Item{
91 items := []Item{
92− NewStringItem("1", "Item 1"),
92+ NewStringItem("Item 1"),
93− NewStringItem("2", "Item 2"),
93+ NewStringItem("Item 2"),
94 }
94 }
95
95
96 l := New(items...)
96 l := New(items...)
@@ -119,7 +119,7 @@ func TestRenderVsDrawConsistency(t *testing.T) {
119func BenchmarkRender(b *testing.B) {
119func BenchmarkRender(b *testing.B) {
120 items := make([]Item, 100)
120 items := make([]Item, 100)
121 for i := range items {
121 for i := range items {
122− items[i] = NewStringItem(string(rune(i)), "Item content here")
122+ items[i] = NewStringItem("Item content here")
123 }
123 }
124
124
125 l := New(items...)
125 l := New(items...)
@@ -135,7 +135,7 @@ func BenchmarkRender(b *testing.B) {
135func BenchmarkRenderWithScrolling(b *testing.B) {
135func BenchmarkRenderWithScrolling(b *testing.B) {
136 items := make([]Item, 1000)
136 items := make([]Item, 1000)
137 for i := range items {
137 for i := range items {
138− items[i] = NewStringItem(string(rune(i)), "Item content here")
138+ items[i] = NewStringItem("Item content here")
139 }
139 }
140
140
141 l := New(items...)
141 l := New(items...)
@@ -150,7 +150,7 @@ func BenchmarkRenderWithScrolling(b *testing.B) {
150}
150}
151
151
152func TestStringItemCache(t *testing.T) {
152func TestStringItemCache(t *testing.T) {
153− item := NewStringItem("1", "Test content")
153+ item := NewStringItem("Test content")
154
154
155 // First draw at width 80 should populate cache
155 // First draw at width 80 should populate cache
156 screen1 := uv.NewScreenBuffer(80, 5)
156 screen1 := uv.NewScreenBuffer(80, 5)
@@ -188,14 +188,14 @@ func TestStringItemCache(t *testing.T) {
188
188
189func TestWrappingItemHeight(t *testing.T) {
189func TestWrappingItemHeight(t *testing.T) {
190 // Short text that fits in one line
190 // Short text that fits in one line
191− item1 := NewWrappingStringItem("1", "Short")
191+ item1 := NewWrappingStringItem("Short")
192 if h := item1.Height(80); h != 1 {
192 if h := item1.Height(80); h != 1 {
193 t.Errorf("expected height 1 for short text, got %d", h)
193 t.Errorf("expected height 1 for short text, got %d", h)
194 }
194 }
195
195
196 // Long text that wraps
196 // Long text that wraps
197 longText := "This is a very long line that will definitely wrap when constrained to a narrow width"
197 longText := "This is a very long line that will definitely wrap when constrained to a narrow width"
198− item2 := NewWrappingStringItem("2", longText)
198+ item2 := NewWrappingStringItem(longText)
199
199
200 // At width 80, should be fewer lines than width 20
200 // At width 80, should be fewer lines than width 20
201 height80 := item2.Height(80)
201 height80 := item2.Height(80)
@@ -207,7 +207,7 @@ func TestWrappingItemHeight(t *testing.T) {
207 }
207 }
208
208
209 // Non-wrapping version should always be 1 line
209 // Non-wrapping version should always be 1 line
210− item3 := NewStringItem("3", longText)
210+ item3 := NewStringItem(longText)
211 if h := item3.Height(20); h != 1 {
211 if h := item3.Height(20); h != 1 {
212 t.Errorf("expected height 1 for non-wrapping item, got %d", h)
212 t.Errorf("expected height 1 for non-wrapping item, got %d", h)
213 }
213 }
@@ -215,11 +215,7 @@ func TestWrappingItemHeight(t *testing.T) {
215
215
216func TestMarkdownItemBasic(t *testing.T) {
216func TestMarkdownItemBasic(t *testing.T) {
217 markdown := "# Hello\n\nThis is a **test**."
217 markdown := "# Hello\n\nThis is a **test**."
218− item := NewMarkdownItem("1", markdown)
218+ item := NewMarkdownItem(markdown)
219−
220− if item.ID() != "1" {
221− t.Errorf("expected ID '1', got '%s'", item.ID())
222− }
223
219
224 // Test that height is calculated
220 // Test that height is calculated
225 height := item.Height(80)
221 height := item.Height(80)
@@ -241,7 +237,7 @@ func TestMarkdownItemBasic(t *testing.T) {
241
237
242func TestMarkdownItemCache(t *testing.T) {
238func TestMarkdownItemCache(t *testing.T) {
243 markdown := "# Test\n\nSome content."
239 markdown := "# Test\n\nSome content."
244− item := NewMarkdownItem("1", markdown)
240+ item := NewMarkdownItem(markdown)
245
241
246 // First render at width 80 should populate cache
242 // First render at width 80 should populate cache
247 height1 := item.Height(80)
243 height1 := item.Height(80)
@@ -267,7 +263,7 @@ func TestMarkdownItemCache(t *testing.T) {
267
263
268func TestMarkdownItemMaxCacheWidth(t *testing.T) {
264func TestMarkdownItemMaxCacheWidth(t *testing.T) {
269 markdown := "# Test\n\nSome content."
265 markdown := "# Test\n\nSome content."
270− item := NewMarkdownItem("1", markdown).WithMaxWidth(50)
266+ item := NewMarkdownItem(markdown).WithMaxWidth(50)
271
267
272 // Render at width 40 (below limit) - should cache at width 40
268 // Render at width 40 (below limit) - should cache at width 40
273 _ = item.Height(40)
269 _ = item.Height(40)
@@ -302,7 +298,7 @@ func TestMarkdownItemWithStyleConfig(t *testing.T) {
302 },
298 },
303 }
299 }
304
300
305− item := NewMarkdownItem("1", markdown).WithStyleConfig(styleConfig)
301+ item := NewMarkdownItem(markdown).WithStyleConfig(styleConfig)
306
302
307 // Render should use the custom style
303 // Render should use the custom style
308 height := item.Height(80)
304 height := item.Height(80)
@@ -323,9 +319,9 @@ func TestMarkdownItemWithStyleConfig(t *testing.T) {
323
319
324func TestMarkdownItemInList(t *testing.T) {
320func TestMarkdownItemInList(t *testing.T) {
325 items := []Item{
321 items := []Item{
326− NewMarkdownItem("1", "# First\n\nMarkdown item."),
322+ NewMarkdownItem("# First\n\nMarkdown item."),
327− NewMarkdownItem("2", "# Second\n\nAnother item."),
323+ NewMarkdownItem("# Second\n\nAnother item."),
328− NewStringItem("3", "Regular string item"),
324+ NewStringItem("Regular string item"),
329 }
325 }
330
326
331 l := New(items...)
327 l := New(items...)
@@ -353,7 +349,7 @@ func TestMarkdownItemHeightWithWidth(t *testing.T) {
353 // Test that widths are capped to maxWidth
349 // Test that widths are capped to maxWidth
354 markdown := "This is a paragraph with some text."
350 markdown := "This is a paragraph with some text."
355
351
356− item := NewMarkdownItem("1", markdown).WithMaxWidth(50)
352+ item := NewMarkdownItem(markdown).WithMaxWidth(50)
357
353
358 // At width 30 (below limit), should cache and render at width 30
354 // At width 30 (below limit), should cache and render at width 30
359 height30 := item.Height(30)
355 height30 := item.Height(30)
@@ -381,7 +377,7 @@ func TestMarkdownItemHeightWithWidth(t *testing.T) {
381
377
382func BenchmarkMarkdownItemRender(b *testing.B) {
378func BenchmarkMarkdownItemRender(b *testing.B) {
383 markdown := "# Heading\n\nThis is a paragraph with **bold** and *italic* text.\n\n- Item 1\n- Item 2\n- Item 3"
379 markdown := "# Heading\n\nThis is a paragraph with **bold** and *italic* text.\n\n- Item 1\n- Item 2\n- Item 3"
384− item := NewMarkdownItem("1", markdown)
380+ item := NewMarkdownItem(markdown)
385
381
386 // Prime the cache
382 // Prime the cache
387 screen := uv.NewScreenBuffer(80, 10)
383 screen := uv.NewScreenBuffer(80, 10)
@@ -401,7 +397,7 @@ func BenchmarkMarkdownItemUncached(b *testing.B) {
401
397
402 b.ResetTimer()
398 b.ResetTimer()
403 for i := 0; i < b.N; i++ {
399 for i := 0; i < b.N; i++ {
404− item := NewMarkdownItem("1", markdown)
400+ item := NewMarkdownItem(markdown)
405 screen := uv.NewScreenBuffer(80, 10)
401 screen := uv.NewScreenBuffer(80, 10)
406 area := uv.Rect(0, 0, 80, 10)
402 area := uv.Rect(0, 0, 80, 10)
407 item.Draw(&screen, area)
403 item.Draw(&screen, area)
@@ -409,12 +405,7 @@ func BenchmarkMarkdownItemUncached(b *testing.B) {
409}
405}
410
406
411func TestSpacerItem(t *testing.T) {
407func TestSpacerItem(t *testing.T) {
412− spacer := NewSpacerItem("spacer1", 3)
408+ spacer := NewSpacerItem(3)
413−
414− // Check ID
415− if spacer.ID() != "spacer1" {
416− t.Errorf("expected ID 'spacer1', got %q", spacer.ID())
417− }
418
409
419 // Check height
410 // Check height
420 if h := spacer.Height(80); h != 3 {
411 if h := spacer.Height(80); h != 3 {
@@ -444,11 +435,11 @@ func TestSpacerItem(t *testing.T) {
444func TestSpacerItemInList(t *testing.T) {
435func TestSpacerItemInList(t *testing.T) {
445 // Create a list with items separated by spacers
436 // Create a list with items separated by spacers
446 items := []Item{
437 items := []Item{
447− NewStringItem("1", "Item 1"),
438+ NewStringItem("Item 1"),
448− NewSpacerItem("spacer1", 1),
439+ NewSpacerItem(1),
449− NewStringItem("2", "Item 2"),
440+ NewStringItem("Item 2"),
450− NewSpacerItem("spacer2", 2),
441+ NewSpacerItem(2),
451− NewStringItem("3", "Item 3"),
442+ NewStringItem("Item 3"),
452 }
443 }
453
444
454 l := New(items...)
445 l := New(items...)
@@ -477,28 +468,28 @@ func TestSpacerItemInList(t *testing.T) {
477func TestSpacerItemNavigation(t *testing.T) {
468func TestSpacerItemNavigation(t *testing.T) {
478 // Spacers should not be selectable (they're not focusable)
469 // Spacers should not be selectable (they're not focusable)
479 items := []Item{
470 items := []Item{
480− NewStringItem("1", "Item 1"),
471+ NewStringItem("Item 1"),
481− NewSpacerItem("spacer1", 1),
472+ NewSpacerItem(1),
482− NewStringItem("2", "Item 2"),
473+ NewStringItem("Item 2"),
483 }
474 }
484
475
485 l := New(items...)
476 l := New(items...)
486 l.SetSize(20, 10)
477 l.SetSize(20, 10)
487
478
488 // Select first item
479 // Select first item
489− l.SetSelectedIndex(0)
480+ l.SetSelected(0)
490 if l.SelectedIndex() != 0 {
481 if l.SelectedIndex() != 0 {
491 t.Errorf("expected selected index 0, got %d", l.SelectedIndex())
482 t.Errorf("expected selected index 0, got %d", l.SelectedIndex())
492 }
483 }
493
484
494 // Can select the spacer (it's a valid item, just not focusable)
485 // Can select the spacer (it's a valid item, just not focusable)
495− l.SetSelectedIndex(1)
486+ l.SetSelected(1)
496 if l.SelectedIndex() != 1 {
487 if l.SelectedIndex() != 1 {
497 t.Errorf("expected selected index 1, got %d", l.SelectedIndex())
488 t.Errorf("expected selected index 1, got %d", l.SelectedIndex())
498 }
489 }
499
490
500 // Can select item after spacer
491 // Can select item after spacer
501− l.SetSelectedIndex(2)
492+ l.SetSelected(2)
502 if l.SelectedIndex() != 2 {
493 if l.SelectedIndex() != 2 {
503 t.Errorf("expected selected index 2, got %d", l.SelectedIndex())
494 t.Errorf("expected selected index 2, got %d", l.SelectedIndex())
504 }
495 }
@@ -512,11 +503,11 @@ func uintPtr(v uint) *uint {
512func TestListDoesNotEatLastLine(t *testing.T) {
503func TestListDoesNotEatLastLine(t *testing.T) {
513 // Create items that exactly fill the viewport
504 // Create items that exactly fill the viewport
514 items := []Item{
505 items := []Item{
515− NewStringItem("1", "Line 1"),
506+ NewStringItem("Line 1"),
516− NewStringItem("2", "Line 2"),
507+ NewStringItem("Line 2"),
517− NewStringItem("3", "Line 3"),
508+ NewStringItem("Line 3"),
518− NewStringItem("4", "Line 4"),
509+ NewStringItem("Line 4"),
519− NewStringItem("5", "Line 5"),
510+ NewStringItem("Line 5"),
520 }
511 }
521
512
522 // Create list with height exactly matching content (5 lines, no gaps)
513 // Create list with height exactly matching content (5 lines, no gaps)
@@ -527,7 +518,7 @@ func TestListDoesNotEatLastLine(t *testing.T) {
527 output := l.Render()
518 output := l.Render()
528
519
529 // Count actual lines in output
520 // Count actual lines in output
530− lines := strings.Split(strings.TrimRight(output, "\r\n"), "\r\n")
521+ lines := strings.Split(strings.TrimRight(output, "\n"), "\n")
531 actualLineCount := 0
522 actualLineCount := 0
532 for _, line := range lines {
523 for _, line := range lines {
533 if strings.TrimSpace(line) != "" {
524 if strings.TrimSpace(line) != "" {
@@ -560,13 +551,13 @@ func TestListDoesNotEatLastLine(t *testing.T) {
560func TestListWithScrollDoesNotEatLastLine(t *testing.T) {
551func TestListWithScrollDoesNotEatLastLine(t *testing.T) {
561 // Create more items than viewport height
552 // Create more items than viewport height
562 items := []Item{
553 items := []Item{
563− NewStringItem("1", "Item 1"),
554+ NewStringItem("Item 1"),
564− NewStringItem("2", "Item 2"),
555+ NewStringItem("Item 2"),
565− NewStringItem("3", "Item 3"),
556+ NewStringItem("Item 3"),
566− NewStringItem("4", "Item 4"),
557+ NewStringItem("Item 4"),
567− NewStringItem("5", "Item 5"),
558+ NewStringItem("Item 5"),
568− NewStringItem("6", "Item 6"),
559+ NewStringItem("Item 6"),
569− NewStringItem("7", "Item 7"),
560+ NewStringItem("Item 7"),
570 }
561 }
571
562
572 // Viewport shows 3 items at a time
563 // Viewport shows 3 items at a time
internal/ui/list/list.go+111−175
@@ -16,8 +16,7 @@ type List struct {
16 width, height int
16 width, height int
17
17
18 // Data
18 // Data
19− items []Item
19+ items []Item
20− indexMap map[string]int // ID -> index for fast lookup
21
20
22 // Focus & Selection
21 // Focus & Selection
23 focused bool
22 focused bool
@@ -28,23 +27,23 @@ type List struct {
28 totalHeight int
27 totalHeight int
29
28
30 // Item positioning in master buffer
29 // Item positioning in master buffer
31− itemPositions map[string]itemPosition
30+ itemPositions []itemPosition
32
31
33 // Viewport state
32 // Viewport state
34 offset int // Scroll offset in lines from top
33 offset int // Scroll offset in lines from top
35
34
36 // Mouse state
35 // Mouse state
37 mouseDown bool
36 mouseDown bool
38− mouseDownItem string // Item ID where mouse was pressed
37+ mouseDownItem int // Item index where mouse was pressed
39− mouseDownX int // X position in item content (character offset)
38+ mouseDownX int // X position in item content (character offset)
40− mouseDownY int // Y position in item (line offset)
39+ mouseDownY int // Y position in item (line offset)
41− mouseDragItem string // Current item being dragged over
40+ mouseDragItem int // Current item index being dragged over
42− mouseDragX int // Current X in item content
41+ mouseDragX int // Current X in item content
43− mouseDragY int // Current Y in item
42+ mouseDragY int // Current Y in item
44
43
45 // Dirty tracking
44 // Dirty tracking
46 dirty bool
45 dirty bool
47− dirtyItems map[string]bool
46+ dirtyItems map[int]bool
48}
47}
49
48
50type itemPosition struct {
49type itemPosition struct {
@@ -56,15 +55,11 @@ type itemPosition struct {
56func New(items ...Item) *List {
55func New(items ...Item) *List {
57 l := &List{
56 l := &List{
58 items: items,
57 items: items,
59− indexMap: make(map[string]int, len(items)),
60− itemPositions: make(map[string]itemPosition, len(items)),
58+ itemPositions: make([]itemPosition, len(items)),
61− dirtyItems: make(map[string]bool),
59+ dirtyItems: make(map[int]bool),
62 selectedIdx: -1,
60 selectedIdx: -1,
63− }
61+ mouseDownItem: -1,
64−
62+ mouseDragItem: -1,
65− // Build index map
66− for i, item := range items {
67− l.indexMap[item.ID()] = i
68 }
63 }
69
64
70 l.dirty = true
65 l.dirty = true
@@ -251,7 +246,7 @@ func (l *List) rebuildMasterBuffer() {
251
246
252 // Draw each item
247 // Draw each item
253 currentY := 0
248 currentY := 0
254− for _, item := range l.items {
249+ for i, item := range l.items {
255 itemHeight := item.Height(l.width)
250 itemHeight := item.Height(l.width)
256
251
257 // Draw item to master buffer
252 // Draw item to master buffer
@@ -259,7 +254,7 @@ func (l *List) rebuildMasterBuffer() {
259 item.Draw(l.masterBuffer, area)
254 item.Draw(l.masterBuffer, area)
260
255
261 // Store position
256 // Store position
262− l.itemPositions[item.ID()] = itemPosition{
257+ l.itemPositions[i] = itemPosition{
263 startLine: currentY,
258 startLine: currentY,
264 height: itemHeight,
259 height: itemHeight,
265 }
260 }
@@ -269,7 +264,7 @@ func (l *List) rebuildMasterBuffer() {
269 }
264 }
270
265
271 l.dirty = false
266 l.dirty = false
272− l.dirtyItems = make(map[string]bool)
267+ l.dirtyItems = make(map[int]bool)
273}
268}
274
269
275// updateDirtyItems efficiently updates only changed items using slice operations.
270// updateDirtyItems efficiently updates only changed items using slice operations.
@@ -280,21 +275,9 @@ func (l *List) updateDirtyItems() {
280
275
281 // Check if all dirty items have unchanged heights
276 // Check if all dirty items have unchanged heights
282 allSameHeight := true
277 allSameHeight := true
283− for id := range l.dirtyItems {
278+ for idx := range l.dirtyItems {
284− idx, ok := l.indexMap[id]
285− if !ok {
286− continue
287− }
288−
289 item := l.items[idx]
279 item := l.items[idx]
290− pos, ok := l.itemPositions[id]
280+ pos := l.itemPositions[idx]
291− if !ok {
292− l.dirty = true
293− l.dirtyItems = make(map[string]bool)
294− l.rebuildMasterBuffer()
295− return
296− }
297−
298 newHeight := item.Height(l.width)
281 newHeight := item.Height(l.width)
299 if newHeight != pos.height {
282 if newHeight != pos.height {
300 allSameHeight = false
283 allSameHeight = false
@@ -305,10 +288,9 @@ func (l *List) updateDirtyItems() {
305 // Optimization: If all dirty items have unchanged heights, re-render in place
288 // Optimization: If all dirty items have unchanged heights, re-render in place
306 if allSameHeight {
289 if allSameHeight {
307 buf := l.masterBuffer.Buffer
290 buf := l.masterBuffer.Buffer
308− for id := range l.dirtyItems {
291+ for idx := range l.dirtyItems {
309− idx := l.indexMap[id]
310 item := l.items[idx]
292 item := l.items[idx]
311− pos := l.itemPositions[id]
293+ pos := l.itemPositions[idx]
312
294
313 // Clear the item's area
295 // Clear the item's area
314 for y := pos.startLine; y < pos.startLine+pos.height && y < len(buf.Lines); y++ {
296 for y := pos.startLine; y < pos.startLine+pos.height && y < len(buf.Lines); y++ {
@@ -320,23 +302,22 @@ func (l *List) updateDirtyItems() {
320 item.Draw(l.masterBuffer, area)
302 item.Draw(l.masterBuffer, area)
321 }
303 }
322
304
323− l.dirtyItems = make(map[string]bool)
305+ l.dirtyItems = make(map[int]bool)
324 return
306 return
325 }
307 }
326
308
327 // Height changed - full rebuild
309 // Height changed - full rebuild
328 l.dirty = true
310 l.dirty = true
329− l.dirtyItems = make(map[string]bool)
311+ l.dirtyItems = make(map[int]bool)
330 l.rebuildMasterBuffer()
312 l.rebuildMasterBuffer()
331}
313}
332
314
333// updatePositionsBelow updates the startLine for all items below the given index.
315// updatePositionsBelow updates the startLine for all items below the given index.
334func (l *List) updatePositionsBelow(fromIdx int, delta int) {
316func (l *List) updatePositionsBelow(fromIdx int, delta int) {
335 for i := fromIdx + 1; i < len(l.items); i++ {
317 for i := fromIdx + 1; i < len(l.items); i++ {
336− item := l.items[i]
337− pos := l.itemPositions[item.ID()]
318+ pos := l.itemPositions[i]
338 pos.startLine += delta
319 pos.startLine += delta
339− l.itemPositions[item.ID()] = pos
320+ l.itemPositions[i] = pos
340 }
321 }
341}
322}
342
323
@@ -395,13 +376,7 @@ func (l *List) Len() int {
395// SetItems replaces all items in the list.
376// SetItems replaces all items in the list.
396func (l *List) SetItems(items []Item) {
377func (l *List) SetItems(items []Item) {
397 l.items = items
378 l.items = items
398− l.indexMap = make(map[string]int, len(items))
399− l.itemPositions = make(map[string]itemPosition, len(items))
379+ l.itemPositions = make([]itemPosition, len(items))
400−
401− for i, item := range items {
402− l.indexMap[item.ID()] = i
403− }
404−
405 l.dirty = true
380 l.dirty = true
406}
381}
407
382
@@ -410,15 +385,15 @@ func (l *List) Items() []Item {
410 return l.items
385 return l.items
411}
386}
412
387
413−// AppendItem adds an item to the end of the list.
388+// AppendItem adds an item to the end of the list. Returns true if successful.
414−func (l *List) AppendItem(item Item) {
389+func (l *List) AppendItem(item Item) bool {
415 l.items = append(l.items, item)
390 l.items = append(l.items, item)
416− l.indexMap[item.ID()] = len(l.items) - 1
391+ l.itemPositions = append(l.itemPositions, itemPosition{})
417
392
418 // If buffer not built yet, mark dirty for full rebuild
393 // If buffer not built yet, mark dirty for full rebuild
419 if l.masterBuffer == nil || l.width <= 0 {
394 if l.masterBuffer == nil || l.width <= 0 {
420 l.dirty = true
395 l.dirty = true
421− return
396+ return true
422 }
397 }
423
398
424 // Process any pending dirty items before modifying buffer structure
399 // Process any pending dirty items before modifying buffer structure
@@ -442,23 +417,20 @@ func (l *List) AppendItem(item Item) {
442 item.Draw(l.masterBuffer, area)
417 item.Draw(l.masterBuffer, area)
443
418
444 // Update tracking
419 // Update tracking
445− l.itemPositions[item.ID()] = itemPosition{
420+ l.itemPositions[len(l.items)-1] = itemPosition{
446 startLine: startLine,
421 startLine: startLine,
447 height: itemHeight,
422 height: itemHeight,
448 }
423 }
449 l.totalHeight += itemHeight
424 l.totalHeight += itemHeight
425+
426+ return true
450}
427}
451
428
452−// PrependItem adds an item to the beginning of the list.
429+// PrependItem adds an item to the beginning of the list. Returns true if
430+// successful.
453−func (l *List) PrependItem(item Item) {
431+func (l *List) PrependItem(item Item) bool {
454 l.items = append([]Item{item}, l.items...)
432 l.items = append([]Item{item}, l.items...)
455−
433+ l.itemPositions = append([]itemPosition{{}}, l.itemPositions...)
456− // Rebuild index map (all indices shifted)
457− l.indexMap = make(map[string]int, len(l.items))
458− for i, itm := range l.items {
459− l.indexMap[itm.ID()] = i
460− }
461−
462 if l.selectedIdx >= 0 {
434 if l.selectedIdx >= 0 {
463 l.selectedIdx++
435 l.selectedIdx++
464 }
436 }
@@ -466,7 +438,7 @@ func (l *List) PrependItem(item Item) {
466 // If buffer not built yet, mark dirty for full rebuild
438 // If buffer not built yet, mark dirty for full rebuild
467 if l.masterBuffer == nil || l.width <= 0 {
439 if l.masterBuffer == nil || l.width <= 0 {
468 l.dirty = true
440 l.dirty = true
469− return
441+ return true
470 }
442 }
471
443
472 // Process any pending dirty items before modifying buffer structure
444 // Process any pending dirty items before modifying buffer structure
@@ -492,42 +464,41 @@ func (l *List) PrependItem(item Item) {
492 item.Draw(l.masterBuffer, area)
464 item.Draw(l.masterBuffer, area)
493
465
494 // Update all positions (shift everything down)
466 // Update all positions (shift everything down)
495− for i := 1; i < len(l.items); i++ {
467+ for i := range l.itemPositions {
496− itm := l.items[i]
468+ pos := l.itemPositions[i]
497− if pos, ok := l.itemPositions[itm.ID()]; ok {
498− pos.startLine += itemHeight
469+ pos.startLine += itemHeight
499− l.itemPositions[itm.ID()] = pos
470+ l.itemPositions[i] = pos
500− }
501 }
471 }
502
472
503− // Add position for new item
473+ // Add position for new item at start
504− l.itemPositions[item.ID()] = itemPosition{
474+ l.itemPositions[0] = itemPosition{
505 startLine: 0,
475 startLine: 0,
506 height: itemHeight,
476 height: itemHeight,
507 }
477 }
478+
508 l.totalHeight += itemHeight
479 l.totalHeight += itemHeight
480+
481+ return true
509}
482}
510
483
511−// UpdateItem replaces an item with the same ID.
484+// UpdateItem replaces an item with the same index. Returns true if successful.
512−func (l *List) UpdateItem(id string, item Item) {
485+func (l *List) UpdateItem(idx int, item Item) bool {
513− idx, ok := l.indexMap[id]
486+ if idx < 0 || idx >= len(l.items) {
514− if !ok {
515− return
487+ return false
516 }
488 }
517−
518 l.items[idx] = item
489 l.items[idx] = item
519− l.dirtyItems[id] = true
490+ l.dirtyItems[idx] = true
491+ return true
520}
492}
521
493
522−// DeleteItem removes an item by ID.
494+// DeleteItem removes an item by index. Returns true if successful.
523−func (l *List) DeleteItem(id string) {
495+func (l *List) DeleteItem(idx int) bool {
524− idx, ok := l.indexMap[id]
496+ if idx < 0 || idx >= len(l.items) {
525− if !ok {
526− return
497+ return false
527 }
498 }
528
499
529 // Get position before deleting
500 // Get position before deleting
530− pos, hasPos := l.itemPositions[id]
501+ pos := l.itemPositions[idx]
531
502
532 // Process any pending dirty items before modifying buffer structure
503 // Process any pending dirty items before modifying buffer structure
533 if len(l.dirtyItems) > 0 {
504 if len(l.dirtyItems) > 0 {
@@ -535,13 +506,7 @@ func (l *List) DeleteItem(id string) {
535 }
506 }
536
507
537 l.items = append(l.items[:idx], l.items[idx+1:]...)
508 l.items = append(l.items[:idx], l.items[idx+1:]...)
538− delete(l.indexMap, id)
509+ l.itemPositions = append(l.itemPositions[:idx], l.itemPositions[idx+1:]...)
539− delete(l.itemPositions, id)
540−
541− // Rebuild index map for items after deleted one
542− for i := idx; i < len(l.items); i++ {
543− l.indexMap[l.items[i].ID()] = i
544− }
545
510
546 // Adjust selection
511 // Adjust selection
547 if l.selectedIdx == idx {
512 if l.selectedIdx == idx {
@@ -557,9 +522,9 @@ func (l *List) DeleteItem(id string) {
557 }
522 }
558
523
559 // If buffer not built yet, mark dirty for full rebuild
524 // If buffer not built yet, mark dirty for full rebuild
560− if l.masterBuffer == nil || !hasPos {
525+ if l.masterBuffer == nil {
561 l.dirty = true
526 l.dirty = true
562− return
527+ return true
563 }
528 }
564
529
565 // Efficient delete: remove lines from buffer
530 // Efficient delete: remove lines from buffer
@@ -575,6 +540,8 @@ func (l *List) DeleteItem(id string) {
575 // Position data corrupt, rebuild
540 // Position data corrupt, rebuild
576 l.dirty = true
541 l.dirty = true
577 }
542 }
543+
544+ return true
578}
545}
579
546
580// Focus focuses the list and the selected item (if focusable).
547// Focus focuses the list and the selected item (if focusable).
@@ -595,12 +562,10 @@ func (l *List) Focused() bool {
595}
562}
596
563
597// SetSelected sets the selected item by ID.
564// SetSelected sets the selected item by ID.
598−func (l *List) SetSelected(id string) {
565+func (l *List) SetSelected(idx int) {
599− idx, ok := l.indexMap[id]
566+ if idx < 0 || idx >= len(l.items) {
600− if !ok {
601 return
567 return
602 }
568 }
603−
604 if l.selectedIdx == idx {
569 if l.selectedIdx == idx {
605 return
570 return
606 }
571 }
@@ -613,33 +578,25 @@ func (l *List) SetSelected(id string) {
613 if prevIdx >= 0 && prevIdx < len(l.items) {
578 if prevIdx >= 0 && prevIdx < len(l.items) {
614 if f, ok := l.items[prevIdx].(Focusable); ok {
579 if f, ok := l.items[prevIdx].(Focusable); ok {
615 f.Blur()
580 f.Blur()
616− l.dirtyItems[l.items[prevIdx].ID()] = true
581+ l.dirtyItems[prevIdx] = true
617 }
582 }
618 }
583 }
619
584
620 if f, ok := l.items[idx].(Focusable); ok {
585 if f, ok := l.items[idx].(Focusable); ok {
621 f.Focus()
586 f.Focus()
622− l.dirtyItems[l.items[idx].ID()] = true
587+ l.dirtyItems[idx] = true
623 }
588 }
624 }
589 }
625}
590}
626
591
627−// SetSelectedIndex sets the selected item by index.
628−func (l *List) SetSelectedIndex(idx int) {
629− if idx < 0 || idx >= len(l.items) {
630− return
631− }
632− l.SetSelected(l.items[idx].ID())
633−}
634−
635// SelectFirst selects the first item in the list.
592// SelectFirst selects the first item in the list.
636func (l *List) SelectFirst() {
593func (l *List) SelectFirst() {
637− l.SetSelectedIndex(0)
594+ l.SetSelected(0)
638}
595}
639
596
640// SelectLast selects the last item in the list.
597// SelectLast selects the last item in the list.
641func (l *List) SelectLast() {
598func (l *List) SelectLast() {
642− l.SetSelectedIndex(len(l.items) - 1)
599+ l.SetSelected(len(l.items) - 1)
643}
600}
644
601
645// SelectNextWrap selects the next item in the list (wraps to beginning).
602// SelectNextWrap selects the next item in the list (wraps to beginning).
@@ -679,7 +636,7 @@ func (l *List) selectNext(wrap bool) {
679 }
636 }
680
637
681 // Select and scroll to this item
638 // Select and scroll to this item
682− l.SetSelected(l.items[nextIdx].ID())
639+ l.SetSelected(nextIdx)
683 return
640 return
684 }
641 }
685}
642}
@@ -721,7 +678,7 @@ func (l *List) selectPrev(wrap bool) {
721 }
678 }
722
679
723 // Select and scroll to this item
680 // Select and scroll to this item
724− l.SetSelected(l.items[prevIdx].ID())
681+ l.SetSelected(prevIdx)
725 return
682 return
726 }
683 }
727}
684}
@@ -773,13 +730,9 @@ func (l *List) ScrollToBottom() {
773}
730}
774
731
775// ScrollToItem scrolls to make the item with the given ID visible.
732// ScrollToItem scrolls to make the item with the given ID visible.
776−func (l *List) ScrollToItem(id string) {
733+func (l *List) ScrollToItem(idx int) {
777 l.ensureBuilt()
734 l.ensureBuilt()
778− pos, ok := l.itemPositions[id]
735+ pos := l.itemPositions[idx]
779− if !ok {
780− return
781− }
782−
783 itemStart := pos.startLine
736 itemStart := pos.startLine
784 itemEnd := pos.startLine + pos.height
737 itemEnd := pos.startLine + pos.height
785 viewStart := l.offset
738 viewStart := l.offset
@@ -805,7 +758,7 @@ func (l *List) ScrollToSelected() {
805 if l.selectedIdx < 0 || l.selectedIdx >= len(l.items) {
758 if l.selectedIdx < 0 || l.selectedIdx >= len(l.items) {
806 return
759 return
807 }
760 }
808− l.ScrollToItem(l.items[l.selectedIdx].ID())
761+ l.ScrollToItem(l.selectedIdx)
809}
762}
810
763
811// Offset returns the current scroll offset.
764// Offset returns the current scroll offset.
@@ -825,15 +778,12 @@ func (l *List) SelectFirstInView() {
825 viewportStart := l.offset
778 viewportStart := l.offset
826 viewportEnd := l.offset + l.height
779 viewportEnd := l.offset + l.height
827
780
828− for i, item := range l.items {
781+ for i := range l.items {
829− pos, ok := l.itemPositions[item.ID()]
782+ pos := l.itemPositions[i]
830− if !ok {
831− continue
832− }
833
783
834 // Check if item is fully within viewport bounds
784 // Check if item is fully within viewport bounds
835 if pos.startLine >= viewportStart && (pos.startLine+pos.height) <= viewportEnd {
785 if pos.startLine >= viewportStart && (pos.startLine+pos.height) <= viewportEnd {
836− l.SetSelectedIndex(i)
786+ l.SetSelected(i)
837 return
787 return
838 }
788 }
839 }
789 }
@@ -847,15 +797,11 @@ func (l *List) SelectLastInView() {
847 viewportEnd := l.offset + l.height
797 viewportEnd := l.offset + l.height
848
798
849 for i := len(l.items) - 1; i >= 0; i-- {
799 for i := len(l.items) - 1; i >= 0; i-- {
850− item := l.items[i]
851− pos, ok := l.itemPositions[item.ID()]
800+ pos := l.itemPositions[i]
852− if !ok {
853− continue
854− }
855
801
856 // Check if item is fully within viewport bounds
802 // Check if item is fully within viewport bounds
857 if pos.startLine >= viewportStart && (pos.startLine+pos.height) <= viewportEnd {
803 if pos.startLine >= viewportStart && (pos.startLine+pos.height) <= viewportEnd {
858− l.SetSelectedIndex(i)
804+ l.SetSelected(i)
859 return
805 return
860 }
806 }
861 }
807 }
@@ -868,11 +814,7 @@ func (l *List) SelectedItemInView() bool {
868 }
814 }
869
815
870 // Get selected item ID and position
816 // Get selected item ID and position
871− item := l.items[l.selectedIdx]
872− pos, ok := l.itemPositions[item.ID()]
817+ pos := l.itemPositions[l.selectedIdx]
873− if !ok {
874− return false
875− }
876
818
877 // Check if item is within viewport bounds
819 // Check if item is within viewport bounds
878 viewportStart := l.offset
820 viewportStart := l.offset
@@ -896,7 +838,7 @@ func (l *List) focusSelectedItem() {
896 item := l.items[l.selectedIdx]
838 item := l.items[l.selectedIdx]
897 if f, ok := item.(Focusable); ok {
839 if f, ok := item.(Focusable); ok {
898 f.Focus()
840 f.Focus()
899− l.dirtyItems[item.ID()] = true
841+ l.dirtyItems[l.selectedIdx] = true
900 }
842 }
901}
843}
902
844
@@ -909,7 +851,7 @@ func (l *List) blurSelectedItem() {
909 item := l.items[l.selectedIdx]
851 item := l.items[l.selectedIdx]
910 if f, ok := item.(Focusable); ok {
852 if f, ok := item.(Focusable); ok {
911 f.Blur()
853 f.Blur()
912− l.dirtyItems[item.ID()] = true
854+ l.dirtyItems[l.selectedIdx] = true
913 }
855 }
914}
856}
915
857
@@ -923,8 +865,8 @@ func (l *List) HandleMouseDown(x, y int) bool {
923 bufferY := y + l.offset
865 bufferY := y + l.offset
924
866
925 // Find which item was clicked
867 // Find which item was clicked
926− itemID, itemY := l.findItemAtPosition(bufferY)
868+ itemIdx, itemY := l.findItemAtPosition(bufferY)
927− if itemID == "" {
869+ if itemIdx < 0 {
928 return false
870 return false
929 }
871 }
930
872
@@ -933,17 +875,15 @@ func (l *List) HandleMouseDown(x, y int) bool {
933 // Items can interpret this as character offset in their content
875 // Items can interpret this as character offset in their content
934
876
935 l.mouseDown = true
877 l.mouseDown = true
936− l.mouseDownItem = itemID
878+ l.mouseDownItem = itemIdx
937 l.mouseDownX = x
879 l.mouseDownX = x
938 l.mouseDownY = itemY
880 l.mouseDownY = itemY
939− l.mouseDragItem = itemID
881+ l.mouseDragItem = itemIdx
940 l.mouseDragX = x
882 l.mouseDragX = x
941 l.mouseDragY = itemY
883 l.mouseDragY = itemY
942
884
943 // Select the clicked item
885 // Select the clicked item
944− if idx, ok := l.indexMap[itemID]; ok {
886+ l.SetSelected(itemIdx)
945− l.SetSelectedIndex(idx)
946− }
947
887
948 return true
888 return true
949}
889}
@@ -962,12 +902,12 @@ func (l *List) HandleMouseDrag(x, y int) bool {
962 bufferY := y + l.offset
902 bufferY := y + l.offset
963
903
964 // Find which item we're dragging over
904 // Find which item we're dragging over
965− itemID, itemY := l.findItemAtPosition(bufferY)
905+ itemIdx, itemY := l.findItemAtPosition(bufferY)
966− if itemID == "" {
906+ if itemIdx < 0 {
967 return false
907 return false
968 }
908 }
969
909
970− l.mouseDragItem = itemID
910+ l.mouseDragItem = itemIdx
971 l.mouseDragX = x
911 l.mouseDragX = x
972 l.mouseDragY = itemY
912 l.mouseDragY = itemY
973
913
@@ -994,47 +934,46 @@ func (l *List) HandleMouseUp(x, y int) bool {
994
934
995// ClearHighlight clears any active text highlighting.
935// ClearHighlight clears any active text highlighting.
996func (l *List) ClearHighlight() {
936func (l *List) ClearHighlight() {
997− for _, item := range l.items {
937+ for i, item := range l.items {
998 if h, ok := item.(Highlightable); ok {
938 if h, ok := item.(Highlightable); ok {
999 h.SetHighlight(-1, -1, -1, -1)
939 h.SetHighlight(-1, -1, -1, -1)
1000− l.dirtyItems[item.ID()] = true
940+ l.dirtyItems[i] = true
1001 }
941 }
1002 }
942 }
943+ l.mouseDownItem = -1
944+ l.mouseDragItem = -1
1003}
945}
1004
946
1005// findItemAtPosition finds the item at the given master buffer y coordinate.
947// findItemAtPosition finds the item at the given master buffer y coordinate.
1006−// Returns the item ID and the y offset within that item.
948+// Returns the item index and the y offset within that item. It returns -1, -1
949+// if no item is found.
1007−func (l *List) findItemAtPosition(bufferY int) (itemID string, itemY int) {
950+func (l *List) findItemAtPosition(bufferY int) (itemIdx int, itemY int) {
1008 if bufferY < 0 || bufferY >= l.totalHeight {
951 if bufferY < 0 || bufferY >= l.totalHeight {
1009− return "", 0
952+ return -1, -1
1010 }
953 }
1011
954
1012 // Linear search through items to find which one contains this y
955 // Linear search through items to find which one contains this y
1013 // This could be optimized with binary search if needed
956 // This could be optimized with binary search if needed
1014− for _, item := range l.items {
957+ for i := range l.items {
1015− pos, ok := l.itemPositions[item.ID()]
958+ pos := l.itemPositions[i]
1016− if !ok {
1017− continue
1018− }
1019−
1020 if bufferY >= pos.startLine && bufferY < pos.startLine+pos.height {
959 if bufferY >= pos.startLine && bufferY < pos.startLine+pos.height {
1021− return item.ID(), bufferY - pos.startLine
960+ return i, bufferY - pos.startLine
1022 }
961 }
1023 }
962 }
1024
963
1025− return "", 0
964+ return -1, -1
1026}
965}
1027
966
1028// updateHighlight updates the highlight range for highlightable items.
967// updateHighlight updates the highlight range for highlightable items.
1029// Supports highlighting across multiple items and respects drag direction.
968// Supports highlighting across multiple items and respects drag direction.
1030func (l *List) updateHighlight() {
969func (l *List) updateHighlight() {
1031− if l.mouseDownItem == "" {
970+ if l.mouseDownItem < 0 {
1032 return
971 return
1033 }
972 }
1034
973
1035 // Get start and end item indices
974 // Get start and end item indices
1036− downItemIdx := l.indexMap[l.mouseDownItem]
975+ downItemIdx := l.mouseDownItem
1037− dragItemIdx := l.indexMap[l.mouseDragItem]
976+ dragItemIdx := l.mouseDragItem
1038
977
1039 // Determine selection direction
978 // Determine selection direction
1040 draggingDown := dragItemIdx > downItemIdx ||
979 draggingDown := dragItemIdx > downItemIdx ||
@@ -1064,10 +1003,10 @@ func (l *List) updateHighlight() {
1064 }
1003 }
1065
1004
1066 // Clear all highlights first
1005 // Clear all highlights first
1067− for _, item := range l.items {
1006+ for i, item := range l.items {
1068 if h, ok := item.(Highlightable); ok {
1007 if h, ok := item.(Highlightable); ok {
1069 h.SetHighlight(-1, -1, -1, -1)
1008 h.SetHighlight(-1, -1, -1, -1)
1070− l.dirtyItems[item.ID()] = true
1009+ l.dirtyItems[i] = true
1071 }
1010 }
1072 }
1011 }
1073
1012
@@ -1083,18 +1022,18 @@ func (l *List) updateHighlight() {
1083 item.SetHighlight(startLine, startCol, endLine, endCol)
1022 item.SetHighlight(startLine, startCol, endLine, endCol)
1084 } else if idx == startItemIdx {
1023 } else if idx == startItemIdx {
1085 // First item - from start position to end of item
1024 // First item - from start position to end of item
1086− pos := l.itemPositions[l.items[idx].ID()]
1025+ pos := l.itemPositions[idx]
1087 item.SetHighlight(startLine, startCol, pos.height-1, 9999) // 9999 = end of line
1026 item.SetHighlight(startLine, startCol, pos.height-1, 9999) // 9999 = end of line
1088 } else if idx == endItemIdx {
1027 } else if idx == endItemIdx {
1089 // Last item - from start of item to end position
1028 // Last item - from start of item to end position
1090 item.SetHighlight(0, 0, endLine, endCol)
1029 item.SetHighlight(0, 0, endLine, endCol)
1091 } else {
1030 } else {
1092 // Middle item - fully highlighted
1031 // Middle item - fully highlighted
1093− pos := l.itemPositions[l.items[idx].ID()]
1032+ pos := l.itemPositions[idx]
1094 item.SetHighlight(0, 0, pos.height-1, 9999)
1033 item.SetHighlight(0, 0, pos.height-1, 9999)
1095 }
1034 }
1096
1035
1097− l.dirtyItems[l.items[idx].ID()] = true
1036+ l.dirtyItems[idx] = true
1098 }
1037 }
1099}
1038}
1100
1039
@@ -1110,7 +1049,7 @@ func (l *List) GetHighlightedText() string {
1110 var result strings.Builder
1049 var result strings.Builder
1111
1050
1112 // Iterate through items to find highlighted ones
1051 // Iterate through items to find highlighted ones
1113− for _, item := range l.items {
1052+ for i, item := range l.items {
1114 h, ok := item.(Highlightable)
1053 h, ok := item.(Highlightable)
1115 if !ok {
1054 if !ok {
1116 continue
1055 continue
@@ -1121,10 +1060,7 @@ func (l *List) GetHighlightedText() string {
1121 continue
1060 continue
1122 }
1061 }
1123
1062
1124− pos, ok := l.itemPositions[item.ID()]
1063+ pos := l.itemPositions[i]
1125− if !ok {
1126− continue
1127− }
1128
1064
1129 // Extract text from highlighted region in master buffer
1065 // Extract text from highlighted region in master buffer
1130 for y := startLine; y <= endLine && y < pos.height; y++ {
1066 for y := startLine; y <= endLine && y < pos.height; y++ {
internal/ui/list/list_test.go+46−54
@@ -11,9 +11,9 @@ import (
11
11
12func TestNewList(t *testing.T) {
12func TestNewList(t *testing.T) {
13 items := []Item{
13 items := []Item{
14− NewStringItem("1", "Item 1"),
14+ NewStringItem("Item 1"),
15− NewStringItem("2", "Item 2"),
15+ NewStringItem("Item 2"),
16− NewStringItem("3", "Item 3"),
16+ NewStringItem("Item 3"),
17 }
17 }
18
18
19 l := New(items...)
19 l := New(items...)
@@ -30,9 +30,9 @@ func TestNewList(t *testing.T) {
30
30
31func TestListDraw(t *testing.T) {
31func TestListDraw(t *testing.T) {
32 items := []Item{
32 items := []Item{
33− NewStringItem("1", "Item 1"),
33+ NewStringItem("Item 1"),
34− NewStringItem("2", "Item 2"),
34+ NewStringItem("Item 2"),
35− NewStringItem("3", "Item 3"),
35+ NewStringItem("Item 3"),
36 }
36 }
37
37
38 l := New(items...)
38 l := New(items...)
@@ -54,52 +54,44 @@ func TestListDraw(t *testing.T) {
54
54
55func TestListAppendItem(t *testing.T) {
55func TestListAppendItem(t *testing.T) {
56 items := []Item{
56 items := []Item{
57− NewStringItem("1", "Item 1"),
57+ NewStringItem("Item 1"),
58 }
58 }
59
59
60 l := New(items...)
60 l := New(items...)
61− l.AppendItem(NewStringItem("2", "Item 2"))
61+ l.AppendItem(NewStringItem("Item 2"))
62
62
63 if len(l.items) != 2 {
63 if len(l.items) != 2 {
64 t.Errorf("expected 2 items after append, got %d", len(l.items))
64 t.Errorf("expected 2 items after append, got %d", len(l.items))
65 }
65 }
66−
67− if l.items[1].ID() != "2" {
68− t.Errorf("expected item ID '2', got '%s'", l.items[1].ID())
69− }
70}
66}
71
67
72func TestListDeleteItem(t *testing.T) {
68func TestListDeleteItem(t *testing.T) {
73 items := []Item{
69 items := []Item{
74− NewStringItem("1", "Item 1"),
70+ NewStringItem("Item 1"),
75− NewStringItem("2", "Item 2"),
71+ NewStringItem("Item 2"),
76− NewStringItem("3", "Item 3"),
72+ NewStringItem("Item 3"),
77 }
73 }
78
74
79 l := New(items...)
75 l := New(items...)
80− l.DeleteItem("2")
76+ l.DeleteItem(2)
81
77
82 if len(l.items) != 2 {
78 if len(l.items) != 2 {
83 t.Errorf("expected 2 items after delete, got %d", len(l.items))
79 t.Errorf("expected 2 items after delete, got %d", len(l.items))
84 }
80 }
85−
86− if l.items[1].ID() != "3" {
87− t.Errorf("expected item ID '3', got '%s'", l.items[1].ID())
88− }
89}
81}
90
82
91func TestListUpdateItem(t *testing.T) {
83func TestListUpdateItem(t *testing.T) {
92 items := []Item{
84 items := []Item{
93− NewStringItem("1", "Item 1"),
85+ NewStringItem("Item 1"),
94− NewStringItem("2", "Item 2"),
86+ NewStringItem("Item 2"),
95 }
87 }
96
88
97 l := New(items...)
89 l := New(items...)
98 l.SetSize(80, 10)
90 l.SetSize(80, 10)
99
91
100 // Update item
92 // Update item
101− newItem := NewStringItem("2", "Updated Item 2")
93+ newItem := NewStringItem("Updated Item 2")
102− l.UpdateItem("2", newItem)
94+ l.UpdateItem(1, newItem)
103
95
104 if l.items[1].(*StringItem).content != "Updated Item 2" {
96 if l.items[1].(*StringItem).content != "Updated Item 2" {
105 t.Errorf("expected updated content, got '%s'", l.items[1].(*StringItem).content)
97 t.Errorf("expected updated content, got '%s'", l.items[1].(*StringItem).content)
@@ -108,13 +100,13 @@ func TestListUpdateItem(t *testing.T) {
108
100
109func TestListSelection(t *testing.T) {
101func TestListSelection(t *testing.T) {
110 items := []Item{
102 items := []Item{
111− NewStringItem("1", "Item 1"),
103+ NewStringItem("Item 1"),
112− NewStringItem("2", "Item 2"),
104+ NewStringItem("Item 2"),
113− NewStringItem("3", "Item 3"),
105+ NewStringItem("Item 3"),
114 }
106 }
115
107
116 l := New(items...)
108 l := New(items...)
117− l.SetSelectedIndex(0)
109+ l.SetSelected(0)
118
110
119 if l.SelectedIndex() != 0 {
111 if l.SelectedIndex() != 0 {
120 t.Errorf("expected selected index 0, got %d", l.SelectedIndex())
112 t.Errorf("expected selected index 0, got %d", l.SelectedIndex())
@@ -133,11 +125,11 @@ func TestListSelection(t *testing.T) {
133
125
134func TestListScrolling(t *testing.T) {
126func TestListScrolling(t *testing.T) {
135 items := []Item{
127 items := []Item{
136− NewStringItem("1", "Item 1"),
128+ NewStringItem("Item 1"),
137− NewStringItem("2", "Item 2"),
129+ NewStringItem("Item 2"),
138− NewStringItem("3", "Item 3"),
130+ NewStringItem("Item 3"),
139− NewStringItem("4", "Item 4"),
131+ NewStringItem("Item 4"),
140− NewStringItem("5", "Item 5"),
132+ NewStringItem("Item 5"),
141 }
133 }
142
134
143 l := New(items...)
135 l := New(items...)
@@ -208,7 +200,7 @@ func TestListFocus(t *testing.T) {
208
200
209 l := New(items...)
201 l := New(items...)
210 l.SetSize(80, 10)
202 l.SetSize(80, 10)
211− l.SetSelectedIndex(0)
203+ l.SetSelected(0)
212
204
213 // Focus the list
205 // Focus the list
214 l.Focus()
206 l.Focus()
@@ -256,12 +248,12 @@ func TestFocusNavigationAfterAppendingToViewportHeight(t *testing.T) {
256
248
257 // Start with one item
249 // Start with one item
258 items := []Item{
250 items := []Item{
259− NewStringItem("1", "Item 1").WithFocusStyles(&focusStyle, &blurStyle),
251+ NewStringItem("Item 1").WithFocusStyles(&focusStyle, &blurStyle),
260 }
252 }
261
253
262 l := New(items...)
254 l := New(items...)
263 l.SetSize(20, 15) // 15 lines viewport height
255 l.SetSize(20, 15) // 15 lines viewport height
264− l.SetSelectedIndex(0)
256+ l.SetSelected(0)
265 l.Focus()
257 l.Focus()
266
258
267 // Initial draw to build buffer
259 // Initial draw to build buffer
@@ -271,12 +263,12 @@ func TestFocusNavigationAfterAppendingToViewportHeight(t *testing.T) {
271 // Append items until we exceed viewport height
263 // Append items until we exceed viewport height
272 // Each focusable item with border is 5 lines tall
264 // Each focusable item with border is 5 lines tall
273 for i := 2; i <= 4; i++ {
265 for i := 2; i <= 4; i++ {
274− item := NewStringItem(string(rune('0'+i)), "Item "+string(rune('0'+i))).WithFocusStyles(&focusStyle, &blurStyle)
266+ item := NewStringItem("Item "+string(rune('0'+i))).WithFocusStyles(&focusStyle, &blurStyle)
275 l.AppendItem(item)
267 l.AppendItem(item)
276 }
268 }
277
269
278 // Select the last item
270 // Select the last item
279− l.SetSelectedIndex(3)
271+ l.SetSelected(3)
280
272
281 // Draw
273 // Draw
282 screen = uv.NewScreenBuffer(20, 15)
274 screen = uv.NewScreenBuffer(20, 15)
@@ -318,7 +310,7 @@ func TestFocusableItemUpdate(t *testing.T) {
318 BorderForeground(lipgloss.Color("240"))
310 BorderForeground(lipgloss.Color("240"))
319
311
320 // Create a focusable item
312 // Create a focusable item
321− item := NewStringItem("1", "Test Item").WithFocusStyles(&focusStyle, &blurStyle)
313+ item := NewStringItem("Test Item").WithFocusStyles(&focusStyle, &blurStyle)
322
314
323 // Initially not focused - render with blur style
315 // Initially not focused - render with blur style
324 screen1 := uv.NewScreenBuffer(20, 5)
316 screen1 := uv.NewScreenBuffer(20, 5)
@@ -369,14 +361,14 @@ func TestFocusableItemHeightWithBorder(t *testing.T) {
369 Border(lipgloss.RoundedBorder())
361 Border(lipgloss.RoundedBorder())
370
362
371 // Item without styles has height 1
363 // Item without styles has height 1
372− plainItem := NewStringItem("1", "Test")
364+ plainItem := NewStringItem("Test")
373 plainHeight := plainItem.Height(20)
365 plainHeight := plainItem.Height(20)
374 if plainHeight != 1 {
366 if plainHeight != 1 {
375 t.Errorf("expected plain height 1, got %d", plainHeight)
367 t.Errorf("expected plain height 1, got %d", plainHeight)
376 }
368 }
377
369
378 // Item with border should add border height (2 lines)
370 // Item with border should add border height (2 lines)
379− item := NewStringItem("2", "Test").WithFocusStyles(&borderStyle, &borderStyle)
371+ item := NewStringItem("Test").WithFocusStyles(&borderStyle, &borderStyle)
380 itemHeight := item.Height(20)
372 itemHeight := item.Height(20)
381 expectedHeight := 1 + 2 // content + border
373 expectedHeight := 1 + 2 // content + border
382 if itemHeight != expectedHeight {
374 if itemHeight != expectedHeight {
@@ -396,14 +388,14 @@ func TestFocusableItemInList(t *testing.T) {
396
388
397 // Create list with focusable items
389 // Create list with focusable items
398 items := []Item{
390 items := []Item{
399− NewStringItem("1", "Item 1").WithFocusStyles(&focusStyle, &blurStyle),
391+ NewStringItem("Item 1").WithFocusStyles(&focusStyle, &blurStyle),
400− NewStringItem("2", "Item 2").WithFocusStyles(&focusStyle, &blurStyle),
392+ NewStringItem("Item 2").WithFocusStyles(&focusStyle, &blurStyle),
401− NewStringItem("3", "Item 3").WithFocusStyles(&focusStyle, &blurStyle),
393+ NewStringItem("Item 3").WithFocusStyles(&focusStyle, &blurStyle),
402 }
394 }
403
395
404 l := New(items...)
396 l := New(items...)
405 l.SetSize(80, 20)
397 l.SetSize(80, 20)
406− l.SetSelectedIndex(0)
398+ l.SetSelected(0)
407
399
408 // Focus the list
400 // Focus the list
409 l.Focus()
401 l.Focus()
@@ -421,7 +413,7 @@ func TestFocusableItemInList(t *testing.T) {
421 }
413 }
422
414
423 // Select second item
415 // Select second item
424− l.SetSelectedIndex(1)
416+ l.SetSelected(1)
425
417
426 // First item should be blurred, second focused
418 // First item should be blurred, second focused
427 if firstItem.IsFocused() {
419 if firstItem.IsFocused() {
@@ -447,7 +439,7 @@ func TestFocusableItemInList(t *testing.T) {
447
439
448func TestFocusableItemWithNilStyles(t *testing.T) {
440func TestFocusableItemWithNilStyles(t *testing.T) {
449 // Test with nil styles - should render inner item directly
441 // Test with nil styles - should render inner item directly
450− item := NewStringItem("1", "Plain Item").WithFocusStyles(nil, nil)
442+ item := NewStringItem("Plain Item").WithFocusStyles(nil, nil)
451
443
452 // Height should be based on content (no border since styles are nil)
444 // Height should be based on content (no border since styles are nil)
453 itemHeight := item.Height(20)
445 itemHeight := item.Height(20)
@@ -488,7 +480,7 @@ func TestFocusableItemWithOnlyFocusStyle(t *testing.T) {
488 Border(lipgloss.RoundedBorder()).
480 Border(lipgloss.RoundedBorder()).
489 BorderForeground(lipgloss.Color("86"))
481 BorderForeground(lipgloss.Color("86"))
490
482
491− item := NewStringItem("1", "Test").WithFocusStyles(&focusStyle, nil)
483+ item := NewStringItem("Test").WithFocusStyles(&focusStyle, nil)
492
484
493 // When not focused, should use nil blur style (no border)
485 // When not focused, should use nil blur style (no border)
494 screen1 := uv.NewScreenBuffer(20, 5)
486 screen1 := uv.NewScreenBuffer(20, 5)
@@ -519,15 +511,15 @@ func TestFocusableItemLastLineNotEaten(t *testing.T) {
519 BorderForeground(lipgloss.Color("240"))
511 BorderForeground(lipgloss.Color("240"))
520
512
521 items := []Item{
513 items := []Item{
522− NewStringItem("1", "Item 1").WithFocusStyles(&focusStyle, &blurStyle),
514+ NewStringItem("Item 1").WithFocusStyles(&focusStyle, &blurStyle),
523 Gap,
515 Gap,
524− NewStringItem("2", "Item 2").WithFocusStyles(&focusStyle, &blurStyle),
516+ NewStringItem("Item 2").WithFocusStyles(&focusStyle, &blurStyle),
525 Gap,
517 Gap,
526− NewStringItem("3", "Item 3").WithFocusStyles(&focusStyle, &blurStyle),
518+ NewStringItem("Item 3").WithFocusStyles(&focusStyle, &blurStyle),
527 Gap,
519 Gap,
528− NewStringItem("4", "Item 4").WithFocusStyles(&focusStyle, &blurStyle),
520+ NewStringItem("Item 4").WithFocusStyles(&focusStyle, &blurStyle),
529 Gap,
521 Gap,
530− NewStringItem("5", "Item 5").WithFocusStyles(&focusStyle, &blurStyle),
522+ NewStringItem("Item 5").WithFocusStyles(&focusStyle, &blurStyle),
531 }
523 }
532
524
533 // Items with padding(1) and border are 5 lines each
525 // Items with padding(1) and border are 5 lines each
@@ -543,7 +535,7 @@ func TestFocusableItemLastLineNotEaten(t *testing.T) {
543 l.Focus()
535 l.Focus()
544
536
545 // Select last item
537 // Select last item
546− l.SetSelectedIndex(len(items) - 1)
538+ l.SetSelected(len(items) - 1)
547
539
548 // Scroll to bottom
540 // Scroll to bottom
549 l.ScrollToBottom()
541 l.ScrollToBottom()
internal/ui/model/chat.go+10−21
@@ -11,7 +11,6 @@ import (
11 "github.com/charmbracelet/crush/internal/ui/list"
11 "github.com/charmbracelet/crush/internal/ui/list"
12 "github.com/charmbracelet/crush/internal/ui/styles"
12 "github.com/charmbracelet/crush/internal/ui/styles"
13 uv "github.com/charmbracelet/ultraviolet"
13 uv "github.com/charmbracelet/ultraviolet"
14− "github.com/google/uuid"
15)
14)
16
15
17// ChatAnimItem represents a chat animation item in the chat UI.
16// ChatAnimItem represents a chat animation item in the chat UI.
@@ -57,20 +56,15 @@ func (c *ChatAnimItem) Height(int) int {
57 return 1
56 return 1
58}
57}
59
58
60−// ID implements list.Item.
61−func (c *ChatAnimItem) ID() string {
62− return "anim"
63−}
64−
65// ChatNoContentItem represents a chat item with no content.
59// ChatNoContentItem represents a chat item with no content.
66type ChatNoContentItem struct {
60type ChatNoContentItem struct {
67 *list.StringItem
61 *list.StringItem
68}
62}
69
63
70// NewChatNoContentItem creates a new instance of [ChatNoContentItem].
64// NewChatNoContentItem creates a new instance of [ChatNoContentItem].
71−func NewChatNoContentItem(t *styles.Styles, id string) *ChatNoContentItem {
65+func NewChatNoContentItem(t *styles.Styles) *ChatNoContentItem {
72 c := new(ChatNoContentItem)
66 c := new(ChatNoContentItem)
73− c.StringItem = list.NewStringItem(id, "No message content").
67+ c.StringItem = list.NewStringItem("No message content").
74 WithFocusStyles(&t.Chat.NoContentMessage, &t.Chat.NoContentMessage)
68 WithFocusStyles(&t.Chat.NoContentMessage, &t.Chat.NoContentMessage)
75 return c
69 return c
76}
70}
@@ -93,7 +87,7 @@ func NewChatMessageItem(t *styles.Styles, msg message.Message) *ChatMessageItem
93
87
94 switch msg.Role {
88 switch msg.Role {
95 case message.User:
89 case message.User:
96− item := list.NewMarkdownItem(msg.ID, msg.Content().String()).
90+ item := list.NewMarkdownItem(msg.Content().String()).
97 WithFocusStyles(&t.Chat.UserMessageFocused, &t.Chat.UserMessageBlurred)
91 WithFocusStyles(&t.Chat.UserMessageFocused, &t.Chat.UserMessageBlurred)
98 item.SetHighlightStyle(list.LipglossStyleToCellStyler(t.TextSelection))
92 item.SetHighlightStyle(list.LipglossStyleToCellStyler(t.TextSelection))
99 // TODO: Add attachments
93 // TODO: Add attachments
@@ -113,7 +107,7 @@ func NewChatMessageItem(t *styles.Styles, msg message.Message) *ChatMessageItem
113 details := t.Chat.ErrorDetails.Render(finishedData.Details)
107 details := t.Chat.ErrorDetails.Render(finishedData.Details)
114 errContent := fmt.Sprintf("%s %s\n\n%s", tag, title, details)
108 errContent := fmt.Sprintf("%s %s\n\n%s", tag, title, details)
115
109
116− item := list.NewStringItem(msg.ID, errContent).
110+ item := list.NewStringItem(errContent).
117 WithFocusStyles(&t.Chat.AssistantMessageFocused, &t.Chat.AssistantMessageBlurred)
111 WithFocusStyles(&t.Chat.AssistantMessageFocused, &t.Chat.AssistantMessageBlurred)
118
112
119 c.item = item
113 c.item = item
@@ -141,7 +135,7 @@ func NewChatMessageItem(t *styles.Styles, msg message.Message) *ChatMessageItem
141 parts = append(parts, content)
135 parts = append(parts, content)
142 }
136 }
143
137
144− item := list.NewMarkdownItem(msg.ID, strings.Join(parts, "\n")).
138+ item := list.NewMarkdownItem(strings.Join(parts, "\n")).
145 WithFocusStyles(&t.Chat.AssistantMessageFocused, &t.Chat.AssistantMessageBlurred)
139 WithFocusStyles(&t.Chat.AssistantMessageFocused, &t.Chat.AssistantMessageBlurred)
146 item.SetHighlightStyle(list.LipglossStyleToCellStyler(t.TextSelection))
140 item.SetHighlightStyle(list.LipglossStyleToCellStyler(t.TextSelection))
147
141
@@ -161,11 +155,6 @@ func (c *ChatMessageItem) Height(width int) int {
161 return c.item.Height(width)
155 return c.item.Height(width)
162}
156}
163
157
164−// ID implements list.Item.
165−func (c *ChatMessageItem) ID() string {
166− return c.item.ID()
167−}
168−
169// Blur implements list.Focusable.
158// Blur implements list.Focusable.
170func (c *ChatMessageItem) Blur() {
159func (c *ChatMessageItem) Blur() {
171 if blurable, ok := c.item.(list.Focusable); ok {
160 if blurable, ok := c.item.(list.Focusable); ok {
@@ -248,7 +237,7 @@ func (m *Chat) PrependItem(item list.Item) {
248// AppendMessage appends a new message item to the chat list.
237// AppendMessage appends a new message item to the chat list.
249func (m *Chat) AppendMessage(msg message.Message) {
238func (m *Chat) AppendMessage(msg message.Message) {
250 if msg.ID == "" {
239 if msg.ID == "" {
251− m.AppendItem(NewChatNoContentItem(m.com.Styles, uuid.NewString()))
240+ m.AppendItem(NewChatNoContentItem(m.com.Styles))
252 } else {
241 } else {
253 m.AppendItem(NewChatMessageItem(m.com.Styles, msg))
242 m.AppendItem(NewChatMessageItem(m.com.Styles, msg))
254 }
243 }
@@ -258,7 +247,7 @@ func (m *Chat) AppendMessage(msg message.Message) {
258func (m *Chat) AppendItem(item list.Item) {
247func (m *Chat) AppendItem(item list.Item) {
259 if m.Len() > 0 {
248 if m.Len() > 0 {
260 // Always add a spacer between messages
249 // Always add a spacer between messages
261− m.list.AppendItem(list.NewSpacerItem(uuid.NewString(), 1))
250+ m.list.AppendItem(list.NewSpacerItem(1))
262 }
251 }
263 m.list.AppendItem(item)
252 m.list.AppendItem(item)
264}
253}
@@ -298,9 +287,9 @@ func (m *Chat) SelectedItemInView() bool {
298 return m.list.SelectedItemInView()
287 return m.list.SelectedItemInView()
299}
288}
300
289
301−// SetSelectedIndex sets the selected message index in the chat list.
290+// SetSelected sets the selected message index in the chat list.
302−func (m *Chat) SetSelectedIndex(index int) {
291+func (m *Chat) SetSelected(index int) {
303− m.list.SetSelectedIndex(index)
292+ m.list.SetSelected(index)
304}
293}
305
294
306// SelectPrev selects the previous message in the chat list.
295// SelectPrev selects the previous message in the chat list.
internal/ui/model/ui.go+1−1
@@ -326,7 +326,7 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) (cmds []tea.Cmd) {
326 m.focus = uiFocusMain
326 m.focus = uiFocusMain
327 m.textarea.Blur()
327 m.textarea.Blur()
328 m.chat.Focus()
328 m.chat.Focus()
329− m.chat.SetSelectedIndex(m.chat.Len() - 1)
329+ m.chat.SetSelected(m.chat.Len() - 1)
330 }
330 }
331 case key.Matches(msg, m.keyMap.Chat.Up):
331 case key.Matches(msg, m.keyMap.Chat.Up):
332 m.chat.ScrollBy(-1)
332 m.chat.ScrollBy(-1)