Detailed changes
@@ -12,15 +12,15 @@ import (
func Example_basic() {
// Create some items
items := []list.Item{
- list.NewStringItem("1", "First item"),
- list.NewStringItem("2", "Second item"),
- list.NewStringItem("3", "Third item"),
+ list.NewStringItem("First item"),
+ list.NewStringItem("Second item"),
+ list.NewStringItem("Third item"),
}
// Create a list with options
l := list.New(items...)
l.SetSize(80, 10)
- l.SetSelectedIndex(0)
+ l.SetSelected(0)
if true {
l.Focus()
}
@@ -109,7 +109,7 @@ func Example_focusable() {
// Create list with first item selected and focused
l := list.New(items...)
l.SetSize(80, 20)
- l.SetSelectedIndex(0)
+ l.SetSelected(0)
if true {
l.Focus()
}
@@ -127,8 +127,8 @@ func Example_focusable() {
// Example demonstrates dynamic item updates.
func Example_dynamicUpdates() {
items := []list.Item{
- list.NewStringItem("1", "Item 1"),
- list.NewStringItem("2", "Item 2"),
+ list.NewStringItem("Item 1"),
+ list.NewStringItem("Item 2"),
}
l := list.New(items...)
@@ -140,13 +140,13 @@ func Example_dynamicUpdates() {
l.Draw(&screen, area)
// Update an item
- l.UpdateItem("2", list.NewStringItem("2", "Updated Item 2"))
+ l.UpdateItem(2, list.NewStringItem("Updated Item 2"))
// Draw again - only changed item is re-rendered
l.Draw(&screen, area)
// Append a new item
- l.AppendItem(list.NewStringItem("3", "New Item 3"))
+ l.AppendItem(list.NewStringItem("New Item 3"))
// Draw again - master buffer grows efficiently
l.Draw(&screen, area)
@@ -161,7 +161,6 @@ func Example_scrolling() {
items := make([]list.Item, 100)
for i := range items {
items[i] = list.NewStringItem(
- fmt.Sprintf("%d", i),
fmt.Sprintf("Item %d", i),
)
}
@@ -169,7 +168,7 @@ func Example_scrolling() {
// Create list with small viewport
l := list.New(items...)
l.SetSize(80, 10)
- l.SetSelectedIndex(0)
+ l.SetSelected(0)
// Draw initial view (shows items 0-9)
screen := uv.NewScreenBuffer(80, 10)
@@ -181,7 +180,7 @@ func Example_scrolling() {
l.Draw(&screen, area) // Now shows items 5-14
// Jump to specific item
- l.ScrollToItem("50")
+ l.ScrollToItem(50)
l.Draw(&screen, area) // Now shows item 50 and neighbors
// Scroll to bottom
@@ -258,9 +257,9 @@ func Example_variableHeights() {
func Example_markdown() {
// Create markdown items
items := []list.Item{
- list.NewMarkdownItem("1", "# Welcome\n\nThis is a **markdown** item."),
- list.NewMarkdownItem("2", "## Features\n\n- Supports **bold**\n- Supports *italic*\n- Supports `code`"),
- list.NewMarkdownItem("3", "### Code Block\n\n```go\nfunc main() {\n fmt.Println(\"Hello\")\n}\n```"),
+ list.NewMarkdownItem("# Welcome\n\nThis is a **markdown** item."),
+ list.NewMarkdownItem("## Features\n\n- Supports **bold**\n- Supports *italic*\n- Supports `code`"),
+ list.NewMarkdownItem("### Code Block\n\n```go\nfunc main() {\n fmt.Println(\"Hello\")\n}\n```"),
}
// Create list
@@ -60,9 +60,6 @@ func toUVStyle(lgStyle lipgloss.Style) uv.Style {
type Item interface {
uv.Drawable
- // ID returns unique identifier for this item.
- ID() string
-
// Height returns the item's height in lines for the given width.
// This allows items to calculate height based on text wrapping and available space.
Height(width int) int
@@ -272,7 +269,6 @@ func (b *BaseHighlightable) ApplyHighlight(buf *uv.ScreenBuffer, width, height i
type StringItem struct {
BaseFocusable
BaseHighlightable
- id string
content string // Raw content string (may contain ANSI styles)
wrap bool // Whether to wrap text
@@ -305,9 +301,8 @@ func LipglossStyleToCellStyler(lgStyle lipgloss.Style) CellStyler {
}
// NewStringItem creates a new string item with the given ID and content.
-func NewStringItem(id, content string) *StringItem {
+func NewStringItem(content string) *StringItem {
s := &StringItem{
- id: id,
content: content,
wrap: false,
cache: make(map[int]string),
@@ -317,9 +312,8 @@ func NewStringItem(id, content string) *StringItem {
}
// NewWrappingStringItem creates a new string item that wraps text to fit width.
-func NewWrappingStringItem(id, content string) *StringItem {
+func NewWrappingStringItem(content string) *StringItem {
s := &StringItem{
- id: id,
content: content,
wrap: true,
cache: make(map[int]string),
@@ -335,11 +329,6 @@ func (s *StringItem) WithFocusStyles(focusStyle, blurStyle *lipgloss.Style) *Str
return s
}
-// ID implements Item.
-func (s *StringItem) ID() string {
- return s.id
-}
-
// Height implements Item.
func (s *StringItem) Height(width int) int {
// Calculate content width if we have styles
@@ -423,7 +412,6 @@ func (s *StringItem) SetHighlight(startLine, startCol, endLine, endCol int) {
type MarkdownItem struct {
BaseFocusable
BaseHighlightable
- id string
markdown string // Raw markdown content
styleConfig *ansi.StyleConfig // Optional style configuration
maxWidth int // Maximum wrap width (default 120)
@@ -438,9 +426,8 @@ const DefaultMarkdownMaxWidth = 120
// NewMarkdownItem creates a new markdown item with the given ID and markdown content.
// If focusStyle and blurStyle are both non-nil, the item will implement Focusable.
-func NewMarkdownItem(id, markdown string) *MarkdownItem {
+func NewMarkdownItem(markdown string) *MarkdownItem {
m := &MarkdownItem{
- id: id,
markdown: markdown,
maxWidth: DefaultMarkdownMaxWidth,
cache: make(map[int]string),
@@ -468,11 +455,6 @@ func (m *MarkdownItem) WithFocusStyles(focusStyle, blurStyle *lipgloss.Style) *M
return m
}
-// ID implements Item.
-func (m *MarkdownItem) ID() string {
- return m.id
-}
-
// Height implements Item.
func (m *MarkdownItem) Height(width int) int {
// Render the markdown to get its height
@@ -563,30 +545,23 @@ func (m *MarkdownItem) SetHighlight(startLine, startCol, endLine, endCol int) {
}
// Gap is a 1-line spacer item used to add gaps between items.
-var Gap = NewSpacerItem("spacer-gap", 1)
+var Gap = NewSpacerItem(1)
// SpacerItem is an empty item that takes up vertical space.
// Useful for adding gaps between items in a list.
type SpacerItem struct {
- id string
height int
}
var _ Item = (*SpacerItem)(nil)
// NewSpacerItem creates a new spacer item with the given ID and height in lines.
-func NewSpacerItem(id string, height int) *SpacerItem {
+func NewSpacerItem(height int) *SpacerItem {
return &SpacerItem{
- id: id,
height: height,
}
}
-// ID implements Item.
-func (s *SpacerItem) ID() string {
- return s.id
-}
-
// Height implements Item.
func (s *SpacerItem) Height(width int) int {
return s.height
@@ -10,9 +10,9 @@ import (
func TestRenderHelper(t *testing.T) {
items := []Item{
- NewStringItem("1", "Item 1"),
- NewStringItem("2", "Item 2"),
- NewStringItem("3", "Item 3"),
+ NewStringItem("Item 1"),
+ NewStringItem("Item 2"),
+ NewStringItem("Item 3"),
}
l := New(items...)
@@ -39,11 +39,11 @@ func TestRenderHelper(t *testing.T) {
func TestRenderWithScrolling(t *testing.T) {
items := []Item{
- NewStringItem("1", "Item 1"),
- NewStringItem("2", "Item 2"),
- NewStringItem("3", "Item 3"),
- NewStringItem("4", "Item 4"),
- NewStringItem("5", "Item 5"),
+ NewStringItem("Item 1"),
+ NewStringItem("Item 2"),
+ NewStringItem("Item 3"),
+ NewStringItem("Item 4"),
+ NewStringItem("Item 5"),
}
l := New(items...)
@@ -89,8 +89,8 @@ func TestRenderEmptyList(t *testing.T) {
func TestRenderVsDrawConsistency(t *testing.T) {
items := []Item{
- NewStringItem("1", "Item 1"),
- NewStringItem("2", "Item 2"),
+ NewStringItem("Item 1"),
+ NewStringItem("Item 2"),
}
l := New(items...)
@@ -119,7 +119,7 @@ func TestRenderVsDrawConsistency(t *testing.T) {
func BenchmarkRender(b *testing.B) {
items := make([]Item, 100)
for i := range items {
- items[i] = NewStringItem(string(rune(i)), "Item content here")
+ items[i] = NewStringItem("Item content here")
}
l := New(items...)
@@ -135,7 +135,7 @@ func BenchmarkRender(b *testing.B) {
func BenchmarkRenderWithScrolling(b *testing.B) {
items := make([]Item, 1000)
for i := range items {
- items[i] = NewStringItem(string(rune(i)), "Item content here")
+ items[i] = NewStringItem("Item content here")
}
l := New(items...)
@@ -150,7 +150,7 @@ func BenchmarkRenderWithScrolling(b *testing.B) {
}
func TestStringItemCache(t *testing.T) {
- item := NewStringItem("1", "Test content")
+ item := NewStringItem("Test content")
// First draw at width 80 should populate cache
screen1 := uv.NewScreenBuffer(80, 5)
@@ -188,14 +188,14 @@ func TestStringItemCache(t *testing.T) {
func TestWrappingItemHeight(t *testing.T) {
// Short text that fits in one line
- item1 := NewWrappingStringItem("1", "Short")
+ item1 := NewWrappingStringItem("Short")
if h := item1.Height(80); h != 1 {
t.Errorf("expected height 1 for short text, got %d", h)
}
// Long text that wraps
longText := "This is a very long line that will definitely wrap when constrained to a narrow width"
- item2 := NewWrappingStringItem("2", longText)
+ item2 := NewWrappingStringItem(longText)
// At width 80, should be fewer lines than width 20
height80 := item2.Height(80)
@@ -207,7 +207,7 @@ func TestWrappingItemHeight(t *testing.T) {
}
// Non-wrapping version should always be 1 line
- item3 := NewStringItem("3", longText)
+ item3 := NewStringItem(longText)
if h := item3.Height(20); h != 1 {
t.Errorf("expected height 1 for non-wrapping item, got %d", h)
}
@@ -215,11 +215,7 @@ func TestWrappingItemHeight(t *testing.T) {
func TestMarkdownItemBasic(t *testing.T) {
markdown := "# Hello\n\nThis is a **test**."
- item := NewMarkdownItem("1", markdown)
-
- if item.ID() != "1" {
- t.Errorf("expected ID '1', got '%s'", item.ID())
- }
+ item := NewMarkdownItem(markdown)
// Test that height is calculated
height := item.Height(80)
@@ -241,7 +237,7 @@ func TestMarkdownItemBasic(t *testing.T) {
func TestMarkdownItemCache(t *testing.T) {
markdown := "# Test\n\nSome content."
- item := NewMarkdownItem("1", markdown)
+ item := NewMarkdownItem(markdown)
// First render at width 80 should populate cache
height1 := item.Height(80)
@@ -267,7 +263,7 @@ func TestMarkdownItemCache(t *testing.T) {
func TestMarkdownItemMaxCacheWidth(t *testing.T) {
markdown := "# Test\n\nSome content."
- item := NewMarkdownItem("1", markdown).WithMaxWidth(50)
+ item := NewMarkdownItem(markdown).WithMaxWidth(50)
// Render at width 40 (below limit) - should cache at width 40
_ = item.Height(40)
@@ -302,7 +298,7 @@ func TestMarkdownItemWithStyleConfig(t *testing.T) {
},
}
- item := NewMarkdownItem("1", markdown).WithStyleConfig(styleConfig)
+ item := NewMarkdownItem(markdown).WithStyleConfig(styleConfig)
// Render should use the custom style
height := item.Height(80)
@@ -323,9 +319,9 @@ func TestMarkdownItemWithStyleConfig(t *testing.T) {
func TestMarkdownItemInList(t *testing.T) {
items := []Item{
- NewMarkdownItem("1", "# First\n\nMarkdown item."),
- NewMarkdownItem("2", "# Second\n\nAnother item."),
- NewStringItem("3", "Regular string item"),
+ NewMarkdownItem("# First\n\nMarkdown item."),
+ NewMarkdownItem("# Second\n\nAnother item."),
+ NewStringItem("Regular string item"),
}
l := New(items...)
@@ -353,7 +349,7 @@ func TestMarkdownItemHeightWithWidth(t *testing.T) {
// Test that widths are capped to maxWidth
markdown := "This is a paragraph with some text."
- item := NewMarkdownItem("1", markdown).WithMaxWidth(50)
+ item := NewMarkdownItem(markdown).WithMaxWidth(50)
// At width 30 (below limit), should cache and render at width 30
height30 := item.Height(30)
@@ -381,7 +377,7 @@ func TestMarkdownItemHeightWithWidth(t *testing.T) {
func BenchmarkMarkdownItemRender(b *testing.B) {
markdown := "# Heading\n\nThis is a paragraph with **bold** and *italic* text.\n\n- Item 1\n- Item 2\n- Item 3"
- item := NewMarkdownItem("1", markdown)
+ item := NewMarkdownItem(markdown)
// Prime the cache
screen := uv.NewScreenBuffer(80, 10)
@@ -401,7 +397,7 @@ func BenchmarkMarkdownItemUncached(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
- item := NewMarkdownItem("1", markdown)
+ item := NewMarkdownItem(markdown)
screen := uv.NewScreenBuffer(80, 10)
area := uv.Rect(0, 0, 80, 10)
item.Draw(&screen, area)
@@ -409,12 +405,7 @@ func BenchmarkMarkdownItemUncached(b *testing.B) {
}
func TestSpacerItem(t *testing.T) {
- spacer := NewSpacerItem("spacer1", 3)
-
- // Check ID
- if spacer.ID() != "spacer1" {
- t.Errorf("expected ID 'spacer1', got %q", spacer.ID())
- }
+ spacer := NewSpacerItem(3)
// Check height
if h := spacer.Height(80); h != 3 {
@@ -444,11 +435,11 @@ func TestSpacerItem(t *testing.T) {
func TestSpacerItemInList(t *testing.T) {
// Create a list with items separated by spacers
items := []Item{
- NewStringItem("1", "Item 1"),
- NewSpacerItem("spacer1", 1),
- NewStringItem("2", "Item 2"),
- NewSpacerItem("spacer2", 2),
- NewStringItem("3", "Item 3"),
+ NewStringItem("Item 1"),
+ NewSpacerItem(1),
+ NewStringItem("Item 2"),
+ NewSpacerItem(2),
+ NewStringItem("Item 3"),
}
l := New(items...)
@@ -477,28 +468,28 @@ func TestSpacerItemInList(t *testing.T) {
func TestSpacerItemNavigation(t *testing.T) {
// Spacers should not be selectable (they're not focusable)
items := []Item{
- NewStringItem("1", "Item 1"),
- NewSpacerItem("spacer1", 1),
- NewStringItem("2", "Item 2"),
+ NewStringItem("Item 1"),
+ NewSpacerItem(1),
+ NewStringItem("Item 2"),
}
l := New(items...)
l.SetSize(20, 10)
// Select first item
- l.SetSelectedIndex(0)
+ l.SetSelected(0)
if l.SelectedIndex() != 0 {
t.Errorf("expected selected index 0, got %d", l.SelectedIndex())
}
// Can select the spacer (it's a valid item, just not focusable)
- l.SetSelectedIndex(1)
+ l.SetSelected(1)
if l.SelectedIndex() != 1 {
t.Errorf("expected selected index 1, got %d", l.SelectedIndex())
}
// Can select item after spacer
- l.SetSelectedIndex(2)
+ l.SetSelected(2)
if l.SelectedIndex() != 2 {
t.Errorf("expected selected index 2, got %d", l.SelectedIndex())
}
@@ -512,11 +503,11 @@ func uintPtr(v uint) *uint {
func TestListDoesNotEatLastLine(t *testing.T) {
// Create items that exactly fill the viewport
items := []Item{
- NewStringItem("1", "Line 1"),
- NewStringItem("2", "Line 2"),
- NewStringItem("3", "Line 3"),
- NewStringItem("4", "Line 4"),
- NewStringItem("5", "Line 5"),
+ NewStringItem("Line 1"),
+ NewStringItem("Line 2"),
+ NewStringItem("Line 3"),
+ NewStringItem("Line 4"),
+ NewStringItem("Line 5"),
}
// Create list with height exactly matching content (5 lines, no gaps)
@@ -527,7 +518,7 @@ func TestListDoesNotEatLastLine(t *testing.T) {
output := l.Render()
// Count actual lines in output
- lines := strings.Split(strings.TrimRight(output, "\r\n"), "\r\n")
+ lines := strings.Split(strings.TrimRight(output, "\n"), "\n")
actualLineCount := 0
for _, line := range lines {
if strings.TrimSpace(line) != "" {
@@ -560,13 +551,13 @@ func TestListDoesNotEatLastLine(t *testing.T) {
func TestListWithScrollDoesNotEatLastLine(t *testing.T) {
// Create more items than viewport height
items := []Item{
- NewStringItem("1", "Item 1"),
- NewStringItem("2", "Item 2"),
- NewStringItem("3", "Item 3"),
- NewStringItem("4", "Item 4"),
- NewStringItem("5", "Item 5"),
- NewStringItem("6", "Item 6"),
- NewStringItem("7", "Item 7"),
+ NewStringItem("Item 1"),
+ NewStringItem("Item 2"),
+ NewStringItem("Item 3"),
+ NewStringItem("Item 4"),
+ NewStringItem("Item 5"),
+ NewStringItem("Item 6"),
+ NewStringItem("Item 7"),
}
// Viewport shows 3 items at a time
@@ -16,8 +16,7 @@ type List struct {
width, height int
// Data
- items []Item
- indexMap map[string]int // ID -> index for fast lookup
+ items []Item
// Focus & Selection
focused bool
@@ -28,23 +27,23 @@ type List struct {
totalHeight int
// Item positioning in master buffer
- itemPositions map[string]itemPosition
+ itemPositions []itemPosition
// Viewport state
offset int // Scroll offset in lines from top
// Mouse state
mouseDown bool
- mouseDownItem string // Item ID where mouse was pressed
- mouseDownX int // X position in item content (character offset)
- mouseDownY int // Y position in item (line offset)
- mouseDragItem string // Current item being dragged over
- mouseDragX int // Current X in item content
- mouseDragY int // Current Y in item
+ mouseDownItem int // Item index where mouse was pressed
+ mouseDownX int // X position in item content (character offset)
+ mouseDownY int // Y position in item (line offset)
+ mouseDragItem int // Current item index being dragged over
+ mouseDragX int // Current X in item content
+ mouseDragY int // Current Y in item
// Dirty tracking
dirty bool
- dirtyItems map[string]bool
+ dirtyItems map[int]bool
}
type itemPosition struct {
@@ -56,15 +55,11 @@ type itemPosition struct {
func New(items ...Item) *List {
l := &List{
items: items,
- indexMap: make(map[string]int, len(items)),
- itemPositions: make(map[string]itemPosition, len(items)),
- dirtyItems: make(map[string]bool),
+ itemPositions: make([]itemPosition, len(items)),
+ dirtyItems: make(map[int]bool),
selectedIdx: -1,
- }
-
- // Build index map
- for i, item := range items {
- l.indexMap[item.ID()] = i
+ mouseDownItem: -1,
+ mouseDragItem: -1,
}
l.dirty = true
@@ -251,7 +246,7 @@ func (l *List) rebuildMasterBuffer() {
// Draw each item
currentY := 0
- for _, item := range l.items {
+ for i, item := range l.items {
itemHeight := item.Height(l.width)
// Draw item to master buffer
@@ -259,7 +254,7 @@ func (l *List) rebuildMasterBuffer() {
item.Draw(l.masterBuffer, area)
// Store position
- l.itemPositions[item.ID()] = itemPosition{
+ l.itemPositions[i] = itemPosition{
startLine: currentY,
height: itemHeight,
}
@@ -269,7 +264,7 @@ func (l *List) rebuildMasterBuffer() {
}
l.dirty = false
- l.dirtyItems = make(map[string]bool)
+ l.dirtyItems = make(map[int]bool)
}
// updateDirtyItems efficiently updates only changed items using slice operations.
@@ -280,21 +275,9 @@ func (l *List) updateDirtyItems() {
// Check if all dirty items have unchanged heights
allSameHeight := true
- for id := range l.dirtyItems {
- idx, ok := l.indexMap[id]
- if !ok {
- continue
- }
-
+ for idx := range l.dirtyItems {
item := l.items[idx]
- pos, ok := l.itemPositions[id]
- if !ok {
- l.dirty = true
- l.dirtyItems = make(map[string]bool)
- l.rebuildMasterBuffer()
- return
- }
-
+ pos := l.itemPositions[idx]
newHeight := item.Height(l.width)
if newHeight != pos.height {
allSameHeight = false
@@ -305,10 +288,9 @@ func (l *List) updateDirtyItems() {
// Optimization: If all dirty items have unchanged heights, re-render in place
if allSameHeight {
buf := l.masterBuffer.Buffer
- for id := range l.dirtyItems {
- idx := l.indexMap[id]
+ for idx := range l.dirtyItems {
item := l.items[idx]
- pos := l.itemPositions[id]
+ pos := l.itemPositions[idx]
// Clear the item's area
for y := pos.startLine; y < pos.startLine+pos.height && y < len(buf.Lines); y++ {
@@ -320,23 +302,22 @@ func (l *List) updateDirtyItems() {
item.Draw(l.masterBuffer, area)
}
- l.dirtyItems = make(map[string]bool)
+ l.dirtyItems = make(map[int]bool)
return
}
// Height changed - full rebuild
l.dirty = true
- l.dirtyItems = make(map[string]bool)
+ l.dirtyItems = make(map[int]bool)
l.rebuildMasterBuffer()
}
// updatePositionsBelow updates the startLine for all items below the given index.
func (l *List) updatePositionsBelow(fromIdx int, delta int) {
for i := fromIdx + 1; i < len(l.items); i++ {
- item := l.items[i]
- pos := l.itemPositions[item.ID()]
+ pos := l.itemPositions[i]
pos.startLine += delta
- l.itemPositions[item.ID()] = pos
+ l.itemPositions[i] = pos
}
}
@@ -395,13 +376,7 @@ func (l *List) Len() int {
// SetItems replaces all items in the list.
func (l *List) SetItems(items []Item) {
l.items = items
- l.indexMap = make(map[string]int, len(items))
- l.itemPositions = make(map[string]itemPosition, len(items))
-
- for i, item := range items {
- l.indexMap[item.ID()] = i
- }
-
+ l.itemPositions = make([]itemPosition, len(items))
l.dirty = true
}
@@ -410,15 +385,15 @@ func (l *List) Items() []Item {
return l.items
}
-// AppendItem adds an item to the end of the list.
-func (l *List) AppendItem(item Item) {
+// AppendItem adds an item to the end of the list. Returns true if successful.
+func (l *List) AppendItem(item Item) bool {
l.items = append(l.items, item)
- l.indexMap[item.ID()] = len(l.items) - 1
+ l.itemPositions = append(l.itemPositions, itemPosition{})
// If buffer not built yet, mark dirty for full rebuild
if l.masterBuffer == nil || l.width <= 0 {
l.dirty = true
- return
+ return true
}
// Process any pending dirty items before modifying buffer structure
@@ -442,23 +417,20 @@ func (l *List) AppendItem(item Item) {
item.Draw(l.masterBuffer, area)
// Update tracking
- l.itemPositions[item.ID()] = itemPosition{
+ l.itemPositions[len(l.items)-1] = itemPosition{
startLine: startLine,
height: itemHeight,
}
l.totalHeight += itemHeight
+
+ return true
}
-// PrependItem adds an item to the beginning of the list.
-func (l *List) PrependItem(item Item) {
+// PrependItem adds an item to the beginning of the list. Returns true if
+// successful.
+func (l *List) PrependItem(item Item) bool {
l.items = append([]Item{item}, l.items...)
-
- // Rebuild index map (all indices shifted)
- l.indexMap = make(map[string]int, len(l.items))
- for i, itm := range l.items {
- l.indexMap[itm.ID()] = i
- }
-
+ l.itemPositions = append([]itemPosition{{}}, l.itemPositions...)
if l.selectedIdx >= 0 {
l.selectedIdx++
}
@@ -466,7 +438,7 @@ func (l *List) PrependItem(item Item) {
// If buffer not built yet, mark dirty for full rebuild
if l.masterBuffer == nil || l.width <= 0 {
l.dirty = true
- return
+ return true
}
// Process any pending dirty items before modifying buffer structure
@@ -492,42 +464,41 @@ func (l *List) PrependItem(item Item) {
item.Draw(l.masterBuffer, area)
// Update all positions (shift everything down)
- for i := 1; i < len(l.items); i++ {
- itm := l.items[i]
- if pos, ok := l.itemPositions[itm.ID()]; ok {
- pos.startLine += itemHeight
- l.itemPositions[itm.ID()] = pos
- }
+ for i := range l.itemPositions {
+ pos := l.itemPositions[i]
+ pos.startLine += itemHeight
+ l.itemPositions[i] = pos
}
- // Add position for new item
- l.itemPositions[item.ID()] = itemPosition{
+ // Add position for new item at start
+ l.itemPositions[0] = itemPosition{
startLine: 0,
height: itemHeight,
}
+
l.totalHeight += itemHeight
+
+ return true
}
-// UpdateItem replaces an item with the same ID.
-func (l *List) UpdateItem(id string, item Item) {
- idx, ok := l.indexMap[id]
- if !ok {
- return
+// UpdateItem replaces an item with the same index. Returns true if successful.
+func (l *List) UpdateItem(idx int, item Item) bool {
+ if idx < 0 || idx >= len(l.items) {
+ return false
}
-
l.items[idx] = item
- l.dirtyItems[id] = true
+ l.dirtyItems[idx] = true
+ return true
}
-// DeleteItem removes an item by ID.
-func (l *List) DeleteItem(id string) {
- idx, ok := l.indexMap[id]
- if !ok {
- return
+// DeleteItem removes an item by index. Returns true if successful.
+func (l *List) DeleteItem(idx int) bool {
+ if idx < 0 || idx >= len(l.items) {
+ return false
}
// Get position before deleting
- pos, hasPos := l.itemPositions[id]
+ pos := l.itemPositions[idx]
// Process any pending dirty items before modifying buffer structure
if len(l.dirtyItems) > 0 {
@@ -535,13 +506,7 @@ func (l *List) DeleteItem(id string) {
}
l.items = append(l.items[:idx], l.items[idx+1:]...)
- delete(l.indexMap, id)
- delete(l.itemPositions, id)
-
- // Rebuild index map for items after deleted one
- for i := idx; i < len(l.items); i++ {
- l.indexMap[l.items[i].ID()] = i
- }
+ l.itemPositions = append(l.itemPositions[:idx], l.itemPositions[idx+1:]...)
// Adjust selection
if l.selectedIdx == idx {
@@ -557,9 +522,9 @@ func (l *List) DeleteItem(id string) {
}
// If buffer not built yet, mark dirty for full rebuild
- if l.masterBuffer == nil || !hasPos {
+ if l.masterBuffer == nil {
l.dirty = true
- return
+ return true
}
// Efficient delete: remove lines from buffer
@@ -575,6 +540,8 @@ func (l *List) DeleteItem(id string) {
// Position data corrupt, rebuild
l.dirty = true
}
+
+ return true
}
// Focus focuses the list and the selected item (if focusable).
@@ -595,12 +562,10 @@ func (l *List) Focused() bool {
}
// SetSelected sets the selected item by ID.
-func (l *List) SetSelected(id string) {
- idx, ok := l.indexMap[id]
- if !ok {
+func (l *List) SetSelected(idx int) {
+ if idx < 0 || idx >= len(l.items) {
return
}
-
if l.selectedIdx == idx {
return
}
@@ -613,33 +578,25 @@ func (l *List) SetSelected(id string) {
if prevIdx >= 0 && prevIdx < len(l.items) {
if f, ok := l.items[prevIdx].(Focusable); ok {
f.Blur()
- l.dirtyItems[l.items[prevIdx].ID()] = true
+ l.dirtyItems[prevIdx] = true
}
}
if f, ok := l.items[idx].(Focusable); ok {
f.Focus()
- l.dirtyItems[l.items[idx].ID()] = true
+ l.dirtyItems[idx] = true
}
}
}
-// SetSelectedIndex sets the selected item by index.
-func (l *List) SetSelectedIndex(idx int) {
- if idx < 0 || idx >= len(l.items) {
- return
- }
- l.SetSelected(l.items[idx].ID())
-}
-
// SelectFirst selects the first item in the list.
func (l *List) SelectFirst() {
- l.SetSelectedIndex(0)
+ l.SetSelected(0)
}
// SelectLast selects the last item in the list.
func (l *List) SelectLast() {
- l.SetSelectedIndex(len(l.items) - 1)
+ l.SetSelected(len(l.items) - 1)
}
// SelectNextWrap selects the next item in the list (wraps to beginning).
@@ -679,7 +636,7 @@ func (l *List) selectNext(wrap bool) {
}
// Select and scroll to this item
- l.SetSelected(l.items[nextIdx].ID())
+ l.SetSelected(nextIdx)
return
}
}
@@ -721,7 +678,7 @@ func (l *List) selectPrev(wrap bool) {
}
// Select and scroll to this item
- l.SetSelected(l.items[prevIdx].ID())
+ l.SetSelected(prevIdx)
return
}
}
@@ -773,13 +730,9 @@ func (l *List) ScrollToBottom() {
}
// ScrollToItem scrolls to make the item with the given ID visible.
-func (l *List) ScrollToItem(id string) {
+func (l *List) ScrollToItem(idx int) {
l.ensureBuilt()
- pos, ok := l.itemPositions[id]
- if !ok {
- return
- }
-
+ pos := l.itemPositions[idx]
itemStart := pos.startLine
itemEnd := pos.startLine + pos.height
viewStart := l.offset
@@ -805,7 +758,7 @@ func (l *List) ScrollToSelected() {
if l.selectedIdx < 0 || l.selectedIdx >= len(l.items) {
return
}
- l.ScrollToItem(l.items[l.selectedIdx].ID())
+ l.ScrollToItem(l.selectedIdx)
}
// Offset returns the current scroll offset.
@@ -825,15 +778,12 @@ func (l *List) SelectFirstInView() {
viewportStart := l.offset
viewportEnd := l.offset + l.height
- for i, item := range l.items {
- pos, ok := l.itemPositions[item.ID()]
- if !ok {
- continue
- }
+ for i := range l.items {
+ pos := l.itemPositions[i]
// Check if item is fully within viewport bounds
if pos.startLine >= viewportStart && (pos.startLine+pos.height) <= viewportEnd {
- l.SetSelectedIndex(i)
+ l.SetSelected(i)
return
}
}
@@ -847,15 +797,11 @@ func (l *List) SelectLastInView() {
viewportEnd := l.offset + l.height
for i := len(l.items) - 1; i >= 0; i-- {
- item := l.items[i]
- pos, ok := l.itemPositions[item.ID()]
- if !ok {
- continue
- }
+ pos := l.itemPositions[i]
// Check if item is fully within viewport bounds
if pos.startLine >= viewportStart && (pos.startLine+pos.height) <= viewportEnd {
- l.SetSelectedIndex(i)
+ l.SetSelected(i)
return
}
}
@@ -868,11 +814,7 @@ func (l *List) SelectedItemInView() bool {
}
// Get selected item ID and position
- item := l.items[l.selectedIdx]
- pos, ok := l.itemPositions[item.ID()]
- if !ok {
- return false
- }
+ pos := l.itemPositions[l.selectedIdx]
// Check if item is within viewport bounds
viewportStart := l.offset
@@ -896,7 +838,7 @@ func (l *List) focusSelectedItem() {
item := l.items[l.selectedIdx]
if f, ok := item.(Focusable); ok {
f.Focus()
- l.dirtyItems[item.ID()] = true
+ l.dirtyItems[l.selectedIdx] = true
}
}
@@ -909,7 +851,7 @@ func (l *List) blurSelectedItem() {
item := l.items[l.selectedIdx]
if f, ok := item.(Focusable); ok {
f.Blur()
- l.dirtyItems[item.ID()] = true
+ l.dirtyItems[l.selectedIdx] = true
}
}
@@ -923,8 +865,8 @@ func (l *List) HandleMouseDown(x, y int) bool {
bufferY := y + l.offset
// Find which item was clicked
- itemID, itemY := l.findItemAtPosition(bufferY)
- if itemID == "" {
+ itemIdx, itemY := l.findItemAtPosition(bufferY)
+ if itemIdx < 0 {
return false
}
@@ -933,17 +875,15 @@ func (l *List) HandleMouseDown(x, y int) bool {
// Items can interpret this as character offset in their content
l.mouseDown = true
- l.mouseDownItem = itemID
+ l.mouseDownItem = itemIdx
l.mouseDownX = x
l.mouseDownY = itemY
- l.mouseDragItem = itemID
+ l.mouseDragItem = itemIdx
l.mouseDragX = x
l.mouseDragY = itemY
// Select the clicked item
- if idx, ok := l.indexMap[itemID]; ok {
- l.SetSelectedIndex(idx)
- }
+ l.SetSelected(itemIdx)
return true
}
@@ -962,12 +902,12 @@ func (l *List) HandleMouseDrag(x, y int) bool {
bufferY := y + l.offset
// Find which item we're dragging over
- itemID, itemY := l.findItemAtPosition(bufferY)
- if itemID == "" {
+ itemIdx, itemY := l.findItemAtPosition(bufferY)
+ if itemIdx < 0 {
return false
}
- l.mouseDragItem = itemID
+ l.mouseDragItem = itemIdx
l.mouseDragX = x
l.mouseDragY = itemY
@@ -994,47 +934,46 @@ func (l *List) HandleMouseUp(x, y int) bool {
// ClearHighlight clears any active text highlighting.
func (l *List) ClearHighlight() {
- for _, item := range l.items {
+ for i, item := range l.items {
if h, ok := item.(Highlightable); ok {
h.SetHighlight(-1, -1, -1, -1)
- l.dirtyItems[item.ID()] = true
+ l.dirtyItems[i] = true
}
}
+ l.mouseDownItem = -1
+ l.mouseDragItem = -1
}
// findItemAtPosition finds the item at the given master buffer y coordinate.
-// Returns the item ID and the y offset within that item.
-func (l *List) findItemAtPosition(bufferY int) (itemID string, itemY int) {
+// Returns the item index and the y offset within that item. It returns -1, -1
+// if no item is found.
+func (l *List) findItemAtPosition(bufferY int) (itemIdx int, itemY int) {
if bufferY < 0 || bufferY >= l.totalHeight {
- return "", 0
+ return -1, -1
}
// Linear search through items to find which one contains this y
// This could be optimized with binary search if needed
- for _, item := range l.items {
- pos, ok := l.itemPositions[item.ID()]
- if !ok {
- continue
- }
-
+ for i := range l.items {
+ pos := l.itemPositions[i]
if bufferY >= pos.startLine && bufferY < pos.startLine+pos.height {
- return item.ID(), bufferY - pos.startLine
+ return i, bufferY - pos.startLine
}
}
- return "", 0
+ return -1, -1
}
// updateHighlight updates the highlight range for highlightable items.
// Supports highlighting across multiple items and respects drag direction.
func (l *List) updateHighlight() {
- if l.mouseDownItem == "" {
+ if l.mouseDownItem < 0 {
return
}
// Get start and end item indices
- downItemIdx := l.indexMap[l.mouseDownItem]
- dragItemIdx := l.indexMap[l.mouseDragItem]
+ downItemIdx := l.mouseDownItem
+ dragItemIdx := l.mouseDragItem
// Determine selection direction
draggingDown := dragItemIdx > downItemIdx ||
@@ -1064,10 +1003,10 @@ func (l *List) updateHighlight() {
}
// Clear all highlights first
- for _, item := range l.items {
+ for i, item := range l.items {
if h, ok := item.(Highlightable); ok {
h.SetHighlight(-1, -1, -1, -1)
- l.dirtyItems[item.ID()] = true
+ l.dirtyItems[i] = true
}
}
@@ -1083,18 +1022,18 @@ func (l *List) updateHighlight() {
item.SetHighlight(startLine, startCol, endLine, endCol)
} else if idx == startItemIdx {
// First item - from start position to end of item
- pos := l.itemPositions[l.items[idx].ID()]
+ pos := l.itemPositions[idx]
item.SetHighlight(startLine, startCol, pos.height-1, 9999) // 9999 = end of line
} else if idx == endItemIdx {
// Last item - from start of item to end position
item.SetHighlight(0, 0, endLine, endCol)
} else {
// Middle item - fully highlighted
- pos := l.itemPositions[l.items[idx].ID()]
+ pos := l.itemPositions[idx]
item.SetHighlight(0, 0, pos.height-1, 9999)
}
- l.dirtyItems[l.items[idx].ID()] = true
+ l.dirtyItems[idx] = true
}
}
@@ -1110,7 +1049,7 @@ func (l *List) GetHighlightedText() string {
var result strings.Builder
// Iterate through items to find highlighted ones
- for _, item := range l.items {
+ for i, item := range l.items {
h, ok := item.(Highlightable)
if !ok {
continue
@@ -1121,10 +1060,7 @@ func (l *List) GetHighlightedText() string {
continue
}
- pos, ok := l.itemPositions[item.ID()]
- if !ok {
- continue
- }
+ pos := l.itemPositions[i]
// Extract text from highlighted region in master buffer
for y := startLine; y <= endLine && y < pos.height; y++ {
@@ -11,9 +11,9 @@ import (
func TestNewList(t *testing.T) {
items := []Item{
- NewStringItem("1", "Item 1"),
- NewStringItem("2", "Item 2"),
- NewStringItem("3", "Item 3"),
+ NewStringItem("Item 1"),
+ NewStringItem("Item 2"),
+ NewStringItem("Item 3"),
}
l := New(items...)
@@ -30,9 +30,9 @@ func TestNewList(t *testing.T) {
func TestListDraw(t *testing.T) {
items := []Item{
- NewStringItem("1", "Item 1"),
- NewStringItem("2", "Item 2"),
- NewStringItem("3", "Item 3"),
+ NewStringItem("Item 1"),
+ NewStringItem("Item 2"),
+ NewStringItem("Item 3"),
}
l := New(items...)
@@ -54,52 +54,44 @@ func TestListDraw(t *testing.T) {
func TestListAppendItem(t *testing.T) {
items := []Item{
- NewStringItem("1", "Item 1"),
+ NewStringItem("Item 1"),
}
l := New(items...)
- l.AppendItem(NewStringItem("2", "Item 2"))
+ l.AppendItem(NewStringItem("Item 2"))
if len(l.items) != 2 {
t.Errorf("expected 2 items after append, got %d", len(l.items))
}
-
- if l.items[1].ID() != "2" {
- t.Errorf("expected item ID '2', got '%s'", l.items[1].ID())
- }
}
func TestListDeleteItem(t *testing.T) {
items := []Item{
- NewStringItem("1", "Item 1"),
- NewStringItem("2", "Item 2"),
- NewStringItem("3", "Item 3"),
+ NewStringItem("Item 1"),
+ NewStringItem("Item 2"),
+ NewStringItem("Item 3"),
}
l := New(items...)
- l.DeleteItem("2")
+ l.DeleteItem(2)
if len(l.items) != 2 {
t.Errorf("expected 2 items after delete, got %d", len(l.items))
}
-
- if l.items[1].ID() != "3" {
- t.Errorf("expected item ID '3', got '%s'", l.items[1].ID())
- }
}
func TestListUpdateItem(t *testing.T) {
items := []Item{
- NewStringItem("1", "Item 1"),
- NewStringItem("2", "Item 2"),
+ NewStringItem("Item 1"),
+ NewStringItem("Item 2"),
}
l := New(items...)
l.SetSize(80, 10)
// Update item
- newItem := NewStringItem("2", "Updated Item 2")
- l.UpdateItem("2", newItem)
+ newItem := NewStringItem("Updated Item 2")
+ l.UpdateItem(1, newItem)
if l.items[1].(*StringItem).content != "Updated Item 2" {
t.Errorf("expected updated content, got '%s'", l.items[1].(*StringItem).content)
@@ -108,13 +100,13 @@ func TestListUpdateItem(t *testing.T) {
func TestListSelection(t *testing.T) {
items := []Item{
- NewStringItem("1", "Item 1"),
- NewStringItem("2", "Item 2"),
- NewStringItem("3", "Item 3"),
+ NewStringItem("Item 1"),
+ NewStringItem("Item 2"),
+ NewStringItem("Item 3"),
}
l := New(items...)
- l.SetSelectedIndex(0)
+ l.SetSelected(0)
if l.SelectedIndex() != 0 {
t.Errorf("expected selected index 0, got %d", l.SelectedIndex())
@@ -133,11 +125,11 @@ func TestListSelection(t *testing.T) {
func TestListScrolling(t *testing.T) {
items := []Item{
- NewStringItem("1", "Item 1"),
- NewStringItem("2", "Item 2"),
- NewStringItem("3", "Item 3"),
- NewStringItem("4", "Item 4"),
- NewStringItem("5", "Item 5"),
+ NewStringItem("Item 1"),
+ NewStringItem("Item 2"),
+ NewStringItem("Item 3"),
+ NewStringItem("Item 4"),
+ NewStringItem("Item 5"),
}
l := New(items...)
@@ -208,7 +200,7 @@ func TestListFocus(t *testing.T) {
l := New(items...)
l.SetSize(80, 10)
- l.SetSelectedIndex(0)
+ l.SetSelected(0)
// Focus the list
l.Focus()
@@ -256,12 +248,12 @@ func TestFocusNavigationAfterAppendingToViewportHeight(t *testing.T) {
// Start with one item
items := []Item{
- NewStringItem("1", "Item 1").WithFocusStyles(&focusStyle, &blurStyle),
+ NewStringItem("Item 1").WithFocusStyles(&focusStyle, &blurStyle),
}
l := New(items...)
l.SetSize(20, 15) // 15 lines viewport height
- l.SetSelectedIndex(0)
+ l.SetSelected(0)
l.Focus()
// Initial draw to build buffer
@@ -271,12 +263,12 @@ func TestFocusNavigationAfterAppendingToViewportHeight(t *testing.T) {
// Append items until we exceed viewport height
// Each focusable item with border is 5 lines tall
for i := 2; i <= 4; i++ {
- item := NewStringItem(string(rune('0'+i)), "Item "+string(rune('0'+i))).WithFocusStyles(&focusStyle, &blurStyle)
+ item := NewStringItem("Item "+string(rune('0'+i))).WithFocusStyles(&focusStyle, &blurStyle)
l.AppendItem(item)
}
// Select the last item
- l.SetSelectedIndex(3)
+ l.SetSelected(3)
// Draw
screen = uv.NewScreenBuffer(20, 15)
@@ -318,7 +310,7 @@ func TestFocusableItemUpdate(t *testing.T) {
BorderForeground(lipgloss.Color("240"))
// Create a focusable item
- item := NewStringItem("1", "Test Item").WithFocusStyles(&focusStyle, &blurStyle)
+ item := NewStringItem("Test Item").WithFocusStyles(&focusStyle, &blurStyle)
// Initially not focused - render with blur style
screen1 := uv.NewScreenBuffer(20, 5)
@@ -369,14 +361,14 @@ func TestFocusableItemHeightWithBorder(t *testing.T) {
Border(lipgloss.RoundedBorder())
// Item without styles has height 1
- plainItem := NewStringItem("1", "Test")
+ plainItem := NewStringItem("Test")
plainHeight := plainItem.Height(20)
if plainHeight != 1 {
t.Errorf("expected plain height 1, got %d", plainHeight)
}
// Item with border should add border height (2 lines)
- item := NewStringItem("2", "Test").WithFocusStyles(&borderStyle, &borderStyle)
+ item := NewStringItem("Test").WithFocusStyles(&borderStyle, &borderStyle)
itemHeight := item.Height(20)
expectedHeight := 1 + 2 // content + border
if itemHeight != expectedHeight {
@@ -396,14 +388,14 @@ func TestFocusableItemInList(t *testing.T) {
// Create list with focusable items
items := []Item{
- NewStringItem("1", "Item 1").WithFocusStyles(&focusStyle, &blurStyle),
- NewStringItem("2", "Item 2").WithFocusStyles(&focusStyle, &blurStyle),
- NewStringItem("3", "Item 3").WithFocusStyles(&focusStyle, &blurStyle),
+ NewStringItem("Item 1").WithFocusStyles(&focusStyle, &blurStyle),
+ NewStringItem("Item 2").WithFocusStyles(&focusStyle, &blurStyle),
+ NewStringItem("Item 3").WithFocusStyles(&focusStyle, &blurStyle),
}
l := New(items...)
l.SetSize(80, 20)
- l.SetSelectedIndex(0)
+ l.SetSelected(0)
// Focus the list
l.Focus()
@@ -421,7 +413,7 @@ func TestFocusableItemInList(t *testing.T) {
}
// Select second item
- l.SetSelectedIndex(1)
+ l.SetSelected(1)
// First item should be blurred, second focused
if firstItem.IsFocused() {
@@ -447,7 +439,7 @@ func TestFocusableItemInList(t *testing.T) {
func TestFocusableItemWithNilStyles(t *testing.T) {
// Test with nil styles - should render inner item directly
- item := NewStringItem("1", "Plain Item").WithFocusStyles(nil, nil)
+ item := NewStringItem("Plain Item").WithFocusStyles(nil, nil)
// Height should be based on content (no border since styles are nil)
itemHeight := item.Height(20)
@@ -488,7 +480,7 @@ func TestFocusableItemWithOnlyFocusStyle(t *testing.T) {
Border(lipgloss.RoundedBorder()).
BorderForeground(lipgloss.Color("86"))
- item := NewStringItem("1", "Test").WithFocusStyles(&focusStyle, nil)
+ item := NewStringItem("Test").WithFocusStyles(&focusStyle, nil)
// When not focused, should use nil blur style (no border)
screen1 := uv.NewScreenBuffer(20, 5)
@@ -519,15 +511,15 @@ func TestFocusableItemLastLineNotEaten(t *testing.T) {
BorderForeground(lipgloss.Color("240"))
items := []Item{
- NewStringItem("1", "Item 1").WithFocusStyles(&focusStyle, &blurStyle),
+ NewStringItem("Item 1").WithFocusStyles(&focusStyle, &blurStyle),
Gap,
- NewStringItem("2", "Item 2").WithFocusStyles(&focusStyle, &blurStyle),
+ NewStringItem("Item 2").WithFocusStyles(&focusStyle, &blurStyle),
Gap,
- NewStringItem("3", "Item 3").WithFocusStyles(&focusStyle, &blurStyle),
+ NewStringItem("Item 3").WithFocusStyles(&focusStyle, &blurStyle),
Gap,
- NewStringItem("4", "Item 4").WithFocusStyles(&focusStyle, &blurStyle),
+ NewStringItem("Item 4").WithFocusStyles(&focusStyle, &blurStyle),
Gap,
- NewStringItem("5", "Item 5").WithFocusStyles(&focusStyle, &blurStyle),
+ NewStringItem("Item 5").WithFocusStyles(&focusStyle, &blurStyle),
}
// Items with padding(1) and border are 5 lines each
@@ -543,7 +535,7 @@ func TestFocusableItemLastLineNotEaten(t *testing.T) {
l.Focus()
// Select last item
- l.SetSelectedIndex(len(items) - 1)
+ l.SetSelected(len(items) - 1)
// Scroll to bottom
l.ScrollToBottom()
@@ -11,7 +11,6 @@ import (
"github.com/charmbracelet/crush/internal/ui/list"
"github.com/charmbracelet/crush/internal/ui/styles"
uv "github.com/charmbracelet/ultraviolet"
- "github.com/google/uuid"
)
// ChatAnimItem represents a chat animation item in the chat UI.
@@ -57,20 +56,15 @@ func (c *ChatAnimItem) Height(int) int {
return 1
}
-// ID implements list.Item.
-func (c *ChatAnimItem) ID() string {
- return "anim"
-}
-
// ChatNoContentItem represents a chat item with no content.
type ChatNoContentItem struct {
*list.StringItem
}
// NewChatNoContentItem creates a new instance of [ChatNoContentItem].
-func NewChatNoContentItem(t *styles.Styles, id string) *ChatNoContentItem {
+func NewChatNoContentItem(t *styles.Styles) *ChatNoContentItem {
c := new(ChatNoContentItem)
- c.StringItem = list.NewStringItem(id, "No message content").
+ c.StringItem = list.NewStringItem("No message content").
WithFocusStyles(&t.Chat.NoContentMessage, &t.Chat.NoContentMessage)
return c
}
@@ -93,7 +87,7 @@ func NewChatMessageItem(t *styles.Styles, msg message.Message) *ChatMessageItem
switch msg.Role {
case message.User:
- item := list.NewMarkdownItem(msg.ID, msg.Content().String()).
+ item := list.NewMarkdownItem(msg.Content().String()).
WithFocusStyles(&t.Chat.UserMessageFocused, &t.Chat.UserMessageBlurred)
item.SetHighlightStyle(list.LipglossStyleToCellStyler(t.TextSelection))
// TODO: Add attachments
@@ -113,7 +107,7 @@ func NewChatMessageItem(t *styles.Styles, msg message.Message) *ChatMessageItem
details := t.Chat.ErrorDetails.Render(finishedData.Details)
errContent := fmt.Sprintf("%s %s\n\n%s", tag, title, details)
- item := list.NewStringItem(msg.ID, errContent).
+ item := list.NewStringItem(errContent).
WithFocusStyles(&t.Chat.AssistantMessageFocused, &t.Chat.AssistantMessageBlurred)
c.item = item
@@ -141,7 +135,7 @@ func NewChatMessageItem(t *styles.Styles, msg message.Message) *ChatMessageItem
parts = append(parts, content)
}
- item := list.NewMarkdownItem(msg.ID, strings.Join(parts, "\n")).
+ item := list.NewMarkdownItem(strings.Join(parts, "\n")).
WithFocusStyles(&t.Chat.AssistantMessageFocused, &t.Chat.AssistantMessageBlurred)
item.SetHighlightStyle(list.LipglossStyleToCellStyler(t.TextSelection))
@@ -161,11 +155,6 @@ func (c *ChatMessageItem) Height(width int) int {
return c.item.Height(width)
}
-// ID implements list.Item.
-func (c *ChatMessageItem) ID() string {
- return c.item.ID()
-}
-
// Blur implements list.Focusable.
func (c *ChatMessageItem) Blur() {
if blurable, ok := c.item.(list.Focusable); ok {
@@ -248,7 +237,7 @@ func (m *Chat) PrependItem(item list.Item) {
// AppendMessage appends a new message item to the chat list.
func (m *Chat) AppendMessage(msg message.Message) {
if msg.ID == "" {
- m.AppendItem(NewChatNoContentItem(m.com.Styles, uuid.NewString()))
+ m.AppendItem(NewChatNoContentItem(m.com.Styles))
} else {
m.AppendItem(NewChatMessageItem(m.com.Styles, msg))
}
@@ -258,7 +247,7 @@ func (m *Chat) AppendMessage(msg message.Message) {
func (m *Chat) AppendItem(item list.Item) {
if m.Len() > 0 {
// Always add a spacer between messages
- m.list.AppendItem(list.NewSpacerItem(uuid.NewString(), 1))
+ m.list.AppendItem(list.NewSpacerItem(1))
}
m.list.AppendItem(item)
}
@@ -298,9 +287,9 @@ func (m *Chat) SelectedItemInView() bool {
return m.list.SelectedItemInView()
}
-// SetSelectedIndex sets the selected message index in the chat list.
-func (m *Chat) SetSelectedIndex(index int) {
- m.list.SetSelectedIndex(index)
+// SetSelected sets the selected message index in the chat list.
+func (m *Chat) SetSelected(index int) {
+ m.list.SetSelected(index)
}
// SelectPrev selects the previous message in the chat list.
@@ -326,7 +326,7 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) (cmds []tea.Cmd) {
m.focus = uiFocusMain
m.textarea.Blur()
m.chat.Focus()
- m.chat.SetSelectedIndex(m.chat.Len() - 1)
+ m.chat.SetSelected(m.chat.Len() - 1)
}
case key.Matches(msg, m.keyMap.Chat.Up):
m.chat.ScrollBy(-1)