@@ -16,8 +16,7 @@ type List struct {
1616 width, height int
1717
1818 // Data
19− items []Item
20− indexMap map[string]int // ID -> index for fast lookup
19+ items []Item
2120
2221 // Focus & Selection
2322 focused bool
@@ -28,23 +27,23 @@ type List struct {
2827 totalHeight int
2928
3029 // Item positioning in master buffer
31− itemPositions map[string]itemPosition
30+ itemPositions []itemPosition
3231
3332 // Viewport state
3433 offset int // Scroll offset in lines from top
3534
3635 // Mouse state
3736 mouseDown bool
38− mouseDownItem string // Item ID where mouse was pressed
39− mouseDownX int // X position in item content (character offset)
40− mouseDownY int // Y position in item (line offset)
41− mouseDragItem string // Current item being dragged over
42− mouseDragX int // Current X in item content
43− mouseDragY int // Current Y in item
37+ mouseDownItem int // Item index where mouse was pressed
38+ mouseDownX int // X position in item content (character offset)
39+ mouseDownY int // Y position in item (line offset)
40+ mouseDragItem int // Current item index being dragged over
41+ mouseDragX int // Current X in item content
42+ mouseDragY int // Current Y in item
4443
4544 // Dirty tracking
4645 dirty bool
47− dirtyItems map[string]bool
46+ dirtyItems map[int]bool
4847}
4948
5049type itemPosition struct {
@@ -56,15 +55,11 @@ type itemPosition struct {
5655func New(items ...Item) *List {
5756 l := &List{
5857 items: items,
59− indexMap: make(map[string]int, len(items)),
60− itemPositions: make(map[string]itemPosition, len(items)),
61− dirtyItems: make(map[string]bool),
58+ itemPositions: make([]itemPosition, len(items)),
59+ dirtyItems: make(map[int]bool),
6260 selectedIdx: -1,
63− }
64−
65− // Build index map
66− for i, item := range items {
67− l.indexMap[item.ID()] = i
61+ mouseDownItem: -1,
62+ mouseDragItem: -1,
6863 }
6964
7065 l.dirty = true
@@ -251,7 +246,7 @@ func (l *List) rebuildMasterBuffer() {
251246
252247 // Draw each item
253248 currentY := 0
254− for _, item := range l.items {
249+ for i, item := range l.items {
255250 itemHeight := item.Height(l.width)
256251
257252 // Draw item to master buffer
@@ -259,7 +254,7 @@ func (l *List) rebuildMasterBuffer() {
259254 item.Draw(l.masterBuffer, area)
260255
261256 // Store position
262− l.itemPositions[item.ID()] = itemPosition{
257+ l.itemPositions[i] = itemPosition{
263258 startLine: currentY,
264259 height: itemHeight,
265260 }
@@ -269,7 +264,7 @@ func (l *List) rebuildMasterBuffer() {
269264 }
270265
271266 l.dirty = false
272− l.dirtyItems = make(map[string]bool)
267+ l.dirtyItems = make(map[int]bool)
273268}
274269
275270// updateDirtyItems efficiently updates only changed items using slice operations.
@@ -280,21 +275,9 @@ func (l *List) updateDirtyItems() {
280275
281276 // Check if all dirty items have unchanged heights
282277 allSameHeight := true
283− for id := range l.dirtyItems {
284− idx, ok := l.indexMap[id]
285− if !ok {
286− continue
287− }
288−
278+ for idx := range l.dirtyItems {
289279 item := l.items[idx]
290− pos, ok := l.itemPositions[id]
291− if !ok {
292− l.dirty = true
293− l.dirtyItems = make(map[string]bool)
294− l.rebuildMasterBuffer()
295− return
296− }
297−
280+ pos := l.itemPositions[idx]
298281 newHeight := item.Height(l.width)
299282 if newHeight != pos.height {
300283 allSameHeight = false
@@ -305,10 +288,9 @@ func (l *List) updateDirtyItems() {
305288 // Optimization: If all dirty items have unchanged heights, re-render in place
306289 if allSameHeight {
307290 buf := l.masterBuffer.Buffer
308− for id := range l.dirtyItems {
309− idx := l.indexMap[id]
291+ for idx := range l.dirtyItems {
310292 item := l.items[idx]
311− pos := l.itemPositions[id]
293+ pos := l.itemPositions[idx]
312294
313295 // Clear the item's area
314296 for y := pos.startLine; y < pos.startLine+pos.height && y < len(buf.Lines); y++ {
@@ -320,23 +302,22 @@ func (l *List) updateDirtyItems() {
320302 item.Draw(l.masterBuffer, area)
321303 }
322304
323− l.dirtyItems = make(map[string]bool)
305+ l.dirtyItems = make(map[int]bool)
324306 return
325307 }
326308
327309 // Height changed - full rebuild
328310 l.dirty = true
329− l.dirtyItems = make(map[string]bool)
311+ l.dirtyItems = make(map[int]bool)
330312 l.rebuildMasterBuffer()
331313}
332314
333315// updatePositionsBelow updates the startLine for all items below the given index.
334316func (l *List) updatePositionsBelow(fromIdx int, delta int) {
335317 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]
338319 pos.startLine += delta
339− l.itemPositions[item.ID()] = pos
320+ l.itemPositions[i] = pos
340321 }
341322}
342323
@@ -395,13 +376,7 @@ func (l *List) Len() int {
395376// SetItems replaces all items in the list.
396377func (l *List) SetItems(items []Item) {
397378 l.items = items
398− l.indexMap = make(map[string]int, len(items))
399− l.itemPositions = make(map[string]itemPosition, len(items))
400−
401− for i, item := range items {
402− l.indexMap[item.ID()] = i
403− }
404−
379+ l.itemPositions = make([]itemPosition, len(items))
405380 l.dirty = true
406381}
407382
@@ -410,15 +385,15 @@ func (l *List) Items() []Item {
410385 return l.items
411386}
412387
413−// AppendItem adds an item to the end of the list.
414−func (l *List) AppendItem(item Item) {
388+// AppendItem adds an item to the end of the list. Returns true if successful.
389+func (l *List) AppendItem(item Item) bool {
415390 l.items = append(l.items, item)
416− l.indexMap[item.ID()] = len(l.items) - 1
391+ l.itemPositions = append(l.itemPositions, itemPosition{})
417392
418393 // If buffer not built yet, mark dirty for full rebuild
419394 if l.masterBuffer == nil || l.width <= 0 {
420395 l.dirty = true
421− return
396+ return true
422397 }
423398
424399 // Process any pending dirty items before modifying buffer structure
@@ -442,23 +417,20 @@ func (l *List) AppendItem(item Item) {
442417 item.Draw(l.masterBuffer, area)
443418
444419 // Update tracking
445− l.itemPositions[item.ID()] = itemPosition{
420+ l.itemPositions[len(l.items)-1] = itemPosition{
446421 startLine: startLine,
447422 height: itemHeight,
448423 }
449424 l.totalHeight += itemHeight
425+
426+ return true
450427}
451428
452−// PrependItem adds an item to the beginning of the list.
453−func (l *List) PrependItem(item Item) {
429+// PrependItem adds an item to the beginning of the list. Returns true if
430+// successful.
431+func (l *List) PrependItem(item Item) bool {
454432 l.items = append([]Item{item}, l.items...)
455−
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−
433+ l.itemPositions = append([]itemPosition{{}}, l.itemPositions...)
462434 if l.selectedIdx >= 0 {
463435 l.selectedIdx++
464436 }
@@ -466,7 +438,7 @@ func (l *List) PrependItem(item Item) {
466438 // If buffer not built yet, mark dirty for full rebuild
467439 if l.masterBuffer == nil || l.width <= 0 {
468440 l.dirty = true
469− return
441+ return true
470442 }
471443
472444 // Process any pending dirty items before modifying buffer structure
@@ -492,42 +464,41 @@ func (l *List) PrependItem(item Item) {
492464 item.Draw(l.masterBuffer, area)
493465
494466 // Update all positions (shift everything down)
495− for i := 1; i < len(l.items); i++ {
496− itm := l.items[i]
497− if pos, ok := l.itemPositions[itm.ID()]; ok {
498− pos.startLine += itemHeight
499− l.itemPositions[itm.ID()] = pos
500− }
467+ for i := range l.itemPositions {
468+ pos := l.itemPositions[i]
469+ pos.startLine += itemHeight
470+ l.itemPositions[i] = pos
501471 }
502472
503− // Add position for new item
504− l.itemPositions[item.ID()] = itemPosition{
473+ // Add position for new item at start
474+ l.itemPositions[0] = itemPosition{
505475 startLine: 0,
506476 height: itemHeight,
507477 }
478+
508479 l.totalHeight += itemHeight
480+
481+ return true
509482}
510483
511−// UpdateItem replaces an item with the same ID.
512−func (l *List) UpdateItem(id string, item Item) {
513− idx, ok := l.indexMap[id]
514− if !ok {
515− return
484+// UpdateItem replaces an item with the same index. Returns true if successful.
485+func (l *List) UpdateItem(idx int, item Item) bool {
486+ if idx < 0 || idx >= len(l.items) {
487+ return false
516488 }
517−
518489 l.items[idx] = item
519− l.dirtyItems[id] = true
490+ l.dirtyItems[idx] = true
491+ return true
520492}
521493
522−// DeleteItem removes an item by ID.
523−func (l *List) DeleteItem(id string) {
524− idx, ok := l.indexMap[id]
525− if !ok {
526− return
494+// DeleteItem removes an item by index. Returns true if successful.
495+func (l *List) DeleteItem(idx int) bool {
496+ if idx < 0 || idx >= len(l.items) {
497+ return false
527498 }
528499
529500 // Get position before deleting
530− pos, hasPos := l.itemPositions[id]
501+ pos := l.itemPositions[idx]
531502
532503 // Process any pending dirty items before modifying buffer structure
533504 if len(l.dirtyItems) > 0 {
@@ -535,13 +506,7 @@ func (l *List) DeleteItem(id string) {
535506 }
536507
537508 l.items = append(l.items[:idx], l.items[idx+1:]...)
538− delete(l.indexMap, id)
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− }
509+ l.itemPositions = append(l.itemPositions[:idx], l.itemPositions[idx+1:]...)
545510
546511 // Adjust selection
547512 if l.selectedIdx == idx {
@@ -557,9 +522,9 @@ func (l *List) DeleteItem(id string) {
557522 }
558523
559524 // If buffer not built yet, mark dirty for full rebuild
560− if l.masterBuffer == nil || !hasPos {
525+ if l.masterBuffer == nil {
561526 l.dirty = true
562− return
527+ return true
563528 }
564529
565530 // Efficient delete: remove lines from buffer
@@ -575,6 +540,8 @@ func (l *List) DeleteItem(id string) {
575540 // Position data corrupt, rebuild
576541 l.dirty = true
577542 }
543+
544+ return true
578545}
579546
580547// Focus focuses the list and the selected item (if focusable).
@@ -595,12 +562,10 @@ func (l *List) Focused() bool {
595562}
596563
597564// SetSelected sets the selected item by ID.
598−func (l *List) SetSelected(id string) {
599− idx, ok := l.indexMap[id]
600− if !ok {
565+func (l *List) SetSelected(idx int) {
566+ if idx < 0 || idx >= len(l.items) {
601567 return
602568 }
603−
604569 if l.selectedIdx == idx {
605570 return
606571 }
@@ -613,33 +578,25 @@ func (l *List) SetSelected(id string) {
613578 if prevIdx >= 0 && prevIdx < len(l.items) {
614579 if f, ok := l.items[prevIdx].(Focusable); ok {
615580 f.Blur()
616− l.dirtyItems[l.items[prevIdx].ID()] = true
581+ l.dirtyItems[prevIdx] = true
617582 }
618583 }
619584
620585 if f, ok := l.items[idx].(Focusable); ok {
621586 f.Focus()
622− l.dirtyItems[l.items[idx].ID()] = true
587+ l.dirtyItems[idx] = true
623588 }
624589 }
625590}
626591
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−
635592// SelectFirst selects the first item in the list.
636593func (l *List) SelectFirst() {
637− l.SetSelectedIndex(0)
594+ l.SetSelected(0)
638595}
639596
640597// SelectLast selects the last item in the list.
641598func (l *List) SelectLast() {
642− l.SetSelectedIndex(len(l.items) - 1)
599+ l.SetSelected(len(l.items) - 1)
643600}
644601
645602// SelectNextWrap selects the next item in the list (wraps to beginning).
@@ -679,7 +636,7 @@ func (l *List) selectNext(wrap bool) {
679636 }
680637
681638 // Select and scroll to this item
682− l.SetSelected(l.items[nextIdx].ID())
639+ l.SetSelected(nextIdx)
683640 return
684641 }
685642}
@@ -721,7 +678,7 @@ func (l *List) selectPrev(wrap bool) {
721678 }
722679
723680 // Select and scroll to this item
724− l.SetSelected(l.items[prevIdx].ID())
681+ l.SetSelected(prevIdx)
725682 return
726683 }
727684}
@@ -773,13 +730,9 @@ func (l *List) ScrollToBottom() {
773730}
774731
775732// 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) {
777734 l.ensureBuilt()
778− pos, ok := l.itemPositions[id]
779− if !ok {
780− return
781− }
782−
735+ pos := l.itemPositions[idx]
783736 itemStart := pos.startLine
784737 itemEnd := pos.startLine + pos.height
785738 viewStart := l.offset
@@ -805,7 +758,7 @@ func (l *List) ScrollToSelected() {
805758 if l.selectedIdx < 0 || l.selectedIdx >= len(l.items) {
806759 return
807760 }
808− l.ScrollToItem(l.items[l.selectedIdx].ID())
761+ l.ScrollToItem(l.selectedIdx)
809762}
810763
811764// Offset returns the current scroll offset.
@@ -825,15 +778,12 @@ func (l *List) SelectFirstInView() {
825778 viewportStart := l.offset
826779 viewportEnd := l.offset + l.height
827780
828− for i, item := range l.items {
829− pos, ok := l.itemPositions[item.ID()]
830− if !ok {
831− continue
832− }
781+ for i := range l.items {
782+ pos := l.itemPositions[i]
833783
834784 // Check if item is fully within viewport bounds
835785 if pos.startLine >= viewportStart && (pos.startLine+pos.height) <= viewportEnd {
836− l.SetSelectedIndex(i)
786+ l.SetSelected(i)
837787 return
838788 }
839789 }
@@ -847,15 +797,11 @@ func (l *List) SelectLastInView() {
847797 viewportEnd := l.offset + l.height
848798
849799 for i := len(l.items) - 1; i >= 0; i-- {
850− item := l.items[i]
851− pos, ok := l.itemPositions[item.ID()]
852− if !ok {
853− continue
854− }
800+ pos := l.itemPositions[i]
855801
856802 // Check if item is fully within viewport bounds
857803 if pos.startLine >= viewportStart && (pos.startLine+pos.height) <= viewportEnd {
858− l.SetSelectedIndex(i)
804+ l.SetSelected(i)
859805 return
860806 }
861807 }
@@ -868,11 +814,7 @@ func (l *List) SelectedItemInView() bool {
868814 }
869815
870816 // Get selected item ID and position
871− item := l.items[l.selectedIdx]
872− pos, ok := l.itemPositions[item.ID()]
873− if !ok {
874− return false
875− }
817+ pos := l.itemPositions[l.selectedIdx]
876818
877819 // Check if item is within viewport bounds
878820 viewportStart := l.offset
@@ -896,7 +838,7 @@ func (l *List) focusSelectedItem() {
896838 item := l.items[l.selectedIdx]
897839 if f, ok := item.(Focusable); ok {
898840 f.Focus()
899− l.dirtyItems[item.ID()] = true
841+ l.dirtyItems[l.selectedIdx] = true
900842 }
901843}
902844
@@ -909,7 +851,7 @@ func (l *List) blurSelectedItem() {
909851 item := l.items[l.selectedIdx]
910852 if f, ok := item.(Focusable); ok {
911853 f.Blur()
912− l.dirtyItems[item.ID()] = true
854+ l.dirtyItems[l.selectedIdx] = true
913855 }
914856}
915857
@@ -923,8 +865,8 @@ func (l *List) HandleMouseDown(x, y int) bool {
923865 bufferY := y + l.offset
924866
925867 // Find which item was clicked
926− itemID, itemY := l.findItemAtPosition(bufferY)
927− if itemID == "" {
868+ itemIdx, itemY := l.findItemAtPosition(bufferY)
869+ if itemIdx < 0 {
928870 return false
929871 }
930872
@@ -933,17 +875,15 @@ func (l *List) HandleMouseDown(x, y int) bool {
933875 // Items can interpret this as character offset in their content
934876
935877 l.mouseDown = true
936− l.mouseDownItem = itemID
878+ l.mouseDownItem = itemIdx
937879 l.mouseDownX = x
938880 l.mouseDownY = itemY
939− l.mouseDragItem = itemID
881+ l.mouseDragItem = itemIdx
940882 l.mouseDragX = x
941883 l.mouseDragY = itemY
942884
943885 // Select the clicked item
944− if idx, ok := l.indexMap[itemID]; ok {
945− l.SetSelectedIndex(idx)
946− }
886+ l.SetSelected(itemIdx)
947887
948888 return true
949889}
@@ -962,12 +902,12 @@ func (l *List) HandleMouseDrag(x, y int) bool {
962902 bufferY := y + l.offset
963903
964904 // Find which item we're dragging over
965− itemID, itemY := l.findItemAtPosition(bufferY)
966− if itemID == "" {
905+ itemIdx, itemY := l.findItemAtPosition(bufferY)
906+ if itemIdx < 0 {
967907 return false
968908 }
969909
970− l.mouseDragItem = itemID
910+ l.mouseDragItem = itemIdx
971911 l.mouseDragX = x
972912 l.mouseDragY = itemY
973913
@@ -994,47 +934,46 @@ func (l *List) HandleMouseUp(x, y int) bool {
994934
995935// ClearHighlight clears any active text highlighting.
996936func (l *List) ClearHighlight() {
997− for _, item := range l.items {
937+ for i, item := range l.items {
998938 if h, ok := item.(Highlightable); ok {
999939 h.SetHighlight(-1, -1, -1, -1)
1000− l.dirtyItems[item.ID()] = true
940+ l.dirtyItems[i] = true
1001941 }
1002942 }
943+ l.mouseDownItem = -1
944+ l.mouseDragItem = -1
1003945}
1004946
1005947// findItemAtPosition finds the item at the given master buffer y coordinate.
1006−// Returns the item ID and the y offset within that item.
1007−func (l *List) findItemAtPosition(bufferY int) (itemID string, itemY int) {
948+// Returns the item index and the y offset within that item. It returns -1, -1
949+// if no item is found.
950+func (l *List) findItemAtPosition(bufferY int) (itemIdx int, itemY int) {
1008951 if bufferY < 0 || bufferY >= l.totalHeight {
1009− return "", 0
952+ return -1, -1
1010953 }
1011954
1012955 // Linear search through items to find which one contains this y
1013956 // This could be optimized with binary search if needed
1014− for _, item := range l.items {
1015− pos, ok := l.itemPositions[item.ID()]
1016− if !ok {
1017− continue
1018− }
1019−
957+ for i := range l.items {
958+ pos := l.itemPositions[i]
1020959 if bufferY >= pos.startLine && bufferY < pos.startLine+pos.height {
1021− return item.ID(), bufferY - pos.startLine
960+ return i, bufferY - pos.startLine
1022961 }
1023962 }
1024963
1025− return "", 0
964+ return -1, -1
1026965}
1027966
1028967// updateHighlight updates the highlight range for highlightable items.
1029968// Supports highlighting across multiple items and respects drag direction.
1030969func (l *List) updateHighlight() {
1031− if l.mouseDownItem == "" {
970+ if l.mouseDownItem < 0 {
1032971 return
1033972 }
1034973
1035974 // Get start and end item indices
1036− downItemIdx := l.indexMap[l.mouseDownItem]
1037− dragItemIdx := l.indexMap[l.mouseDragItem]
975+ downItemIdx := l.mouseDownItem
976+ dragItemIdx := l.mouseDragItem
1038977
1039978 // Determine selection direction
1040979 draggingDown := dragItemIdx > downItemIdx ||
@@ -1064,10 +1003,10 @@ func (l *List) updateHighlight() {
10641003 }
10651004
10661005 // Clear all highlights first
1067− for _, item := range l.items {
1006+ for i, item := range l.items {
10681007 if h, ok := item.(Highlightable); ok {
10691008 h.SetHighlight(-1, -1, -1, -1)
1070− l.dirtyItems[item.ID()] = true
1009+ l.dirtyItems[i] = true
10711010 }
10721011 }
10731012
@@ -1083,18 +1022,18 @@ func (l *List) updateHighlight() {
10831022 item.SetHighlight(startLine, startCol, endLine, endCol)
10841023 } else if idx == startItemIdx {
10851024 // First item - from start position to end of item
1086− pos := l.itemPositions[l.items[idx].ID()]
1025+ pos := l.itemPositions[idx]
10871026 item.SetHighlight(startLine, startCol, pos.height-1, 9999) // 9999 = end of line
10881027 } else if idx == endItemIdx {
10891028 // Last item - from start of item to end position
10901029 item.SetHighlight(0, 0, endLine, endCol)
10911030 } else {
10921031 // Middle item - fully highlighted
1093− pos := l.itemPositions[l.items[idx].ID()]
1032+ pos := l.itemPositions[idx]
10941033 item.SetHighlight(0, 0, pos.height-1, 9999)
10951034 }
10961035
1097− l.dirtyItems[l.items[idx].ID()] = true
1036+ l.dirtyItems[idx] = true
10981037 }
10991038}
11001039
@@ -1110,7 +1049,7 @@ func (l *List) GetHighlightedText() string {
11101049 var result strings.Builder
11111050
11121051 // Iterate through items to find highlighted ones
1113− for _, item := range l.items {
1052+ for i, item := range l.items {
11141053 h, ok := item.(Highlightable)
11151054 if !ok {
11161055 continue
@@ -1121,10 +1060,7 @@ func (l *List) GetHighlightedText() string {
11211060 continue
11221061 }
11231062
1124− pos, ok := l.itemPositions[item.ID()]
1125− if !ok {
1126− continue
1127− }
1063+ pos := l.itemPositions[i]
11281064
11291065 // Extract text from highlighted region in master buffer
11301066 for y := startLine; y <= endLine && y < pos.height; y++ {