@@ -1,413 +0,0 @@
-package lazylist
-
-import (
- "log/slog"
- "strings"
-)
-
-// List represents a list of items that can be lazily rendered. A list is
-// always rendered like a chat conversation where items are stacked vertically
-// from top to bottom.
-type List struct {
- // Viewport size
- width, height int
-
- // Items in the list
- items []Item
-
- // Gap between items (0 or less means no gap)
- gap int
-
- // Focus and selection state
- focused bool
- selectedIdx int // The current selected index -1 means no selection
-
- // Item positioning. If a position exists in the map, it means the item has
- // been rendered and measured.
- itemPositions map[int]itemPosition
-
- // Rendered content and cache
- lines []string
- renderedItems map[int]renderedItem
- offsetIdx int // Index of the first visible item in the viewport
- offsetLine int // The offset line from the start of the offsetIdx item (can be negative)
-
- // Dirty tracking
- dirtyItems map[int]struct{}
-}
-
-// renderedItem holds the rendered content and height of an item.
-type renderedItem struct {
- content string
- height int
-}
-
-// itemPosition holds the start and end line of an item in the list.
-type itemPosition struct {
- startLine int
- endLine int
-}
-
-// Height returns the height of item based on its start and end lines.
-func (ip itemPosition) Height() int {
- return ip.endLine - ip.startLine
-}
-
-// NewList creates a new lazy-loaded list.
-func NewList(items ...Item) *List {
- l := new(List)
- l.items = items
- l.itemPositions = make(map[int]itemPosition)
- l.renderedItems = make(map[int]renderedItem)
- l.dirtyItems = make(map[int]struct{})
- return l
-}
-
-// SetSize sets the size of the list viewport.
-func (l *List) SetSize(width, height int) {
- if width != l.width {
- // Mark all rendered items as dirty if width changes because their
- // layout may change.
- for idx := range l.itemPositions {
- l.dirtyItems[idx] = struct{}{}
- }
- }
- l.width = width
- l.height = height
-}
-
-// SetGap sets the gap between items.
-func (l *List) SetGap(gap int) {
- l.gap = gap
-}
-
-// Width returns the width of the list viewport.
-func (l *List) Width() int {
- return l.width
-}
-
-// Height returns the height of the list viewport.
-func (l *List) Height() int {
- return l.height
-}
-
-// Len returns the number of items in the list.
-func (l *List) Len() int {
- return len(l.items)
-}
-
-// renderItem renders the item at the given index and updates its cache and
-// position.
-func (l *List) renderItem(idx int) {
- if idx < 0 || idx >= len(l.items) {
- return
- }
-
- item := l.items[idx]
- rendered := item.Render(l.width)
- height := countLines(rendered)
-
- l.renderedItems[idx] = renderedItem{
- content: rendered,
- height: height,
- }
-
- // Calculate item position
- var startLine int
- if idx == 0 {
- startLine = 0
- } else {
- prevPos, ok := l.itemPositions[idx-1]
- if !ok {
- l.renderItem(idx - 1)
- prevPos = l.itemPositions[idx-1]
- }
- startLine = prevPos.endLine
- if l.gap > 0 {
- startLine += l.gap
- }
- }
- endLine := startLine + height
-
- l.itemPositions[idx] = itemPosition{
- startLine: startLine,
- endLine: endLine,
- }
-}
-
-// ScrollToIndex scrolls the list to the given item index.
-func (l *List) ScrollToIndex(index int) {
- if index < 0 || index >= len(l.items) {
- return
- }
- l.offsetIdx = index
- l.offsetLine = 0
-}
-
-// ScrollBy scrolls the list by the given number of lines.
-func (l *List) ScrollBy(lines int) {
- l.offsetLine += lines
- if l.offsetIdx <= 0 && l.offsetLine < 0 {
- l.offsetIdx = 0
- l.offsetLine = 0
- return
- }
-
- // Adjust offset index and line if needed
- for l.offsetLine < 0 && l.offsetIdx > 0 {
- // Move up to previous item
- l.offsetIdx--
- prevPos, ok := l.itemPositions[l.offsetIdx]
- if !ok {
- l.renderItem(l.offsetIdx)
- prevPos = l.itemPositions[l.offsetIdx]
- }
- l.offsetLine += prevPos.Height()
- if l.gap > 0 {
- l.offsetLine += l.gap
- }
- }
-
- for {
- currentPos, ok := l.itemPositions[l.offsetIdx]
- if !ok {
- l.renderItem(l.offsetIdx)
- currentPos = l.itemPositions[l.offsetIdx]
- }
- if l.offsetLine >= currentPos.Height() {
- // Move down to next item
- l.offsetLine -= currentPos.Height()
- if l.gap > 0 {
- l.offsetLine -= l.gap
- }
- l.offsetIdx++
- if l.offsetIdx >= len(l.items) {
- l.offsetIdx = len(l.items) - 1
- l.offsetLine = currentPos.Height() - 1
- break
- }
- } else {
- break
- }
- }
-}
-
-// findVisibleItems finds the range of items that are visible in the viewport.
-func (l *List) findVisibleItems() (startIdx, endIdx int) {
- startIdx = l.offsetIdx
- endIdx = startIdx + 1
-
- // Render items until we fill the viewport
- visibleHeight := -l.offsetLine
- for endIdx < len(l.items) {
- pos, ok := l.itemPositions[endIdx-1]
- if !ok {
- l.renderItem(endIdx - 1)
- pos = l.itemPositions[endIdx-1]
- }
- visibleHeight += pos.Height()
- if endIdx-1 < len(l.items)-1 && l.gap > 0 {
- visibleHeight += l.gap
- }
- if visibleHeight >= l.height {
- break
- }
- endIdx++
- }
-
- if endIdx > len(l.items)-1 {
- endIdx = len(l.items) - 1
- }
-
- return startIdx, endIdx
-}
-
-// renderLines renders the items between startIdx and endIdx into lines.
-func (l *List) renderLines(startIdx, endIdx int) []string {
- var lines []string
- for idx := startIdx; idx < endIdx+1; idx++ {
- rendered, ok := l.renderedItems[idx]
- if !ok {
- l.renderItem(idx)
- rendered = l.renderedItems[idx]
- }
- itemLines := strings.Split(rendered.content, "\n")
- lines = append(lines, itemLines...)
- if l.gap > 0 && idx < endIdx {
- for i := 0; i < l.gap; i++ {
- lines = append(lines, "")
- }
- }
- }
- return lines
-}
-
-// Render renders the list and returns the visible lines.
-func (l *List) Render() string {
- viewStartIdx, viewEndIdx := l.findVisibleItems()
- slog.Info("Render", "viewStartIdx", viewStartIdx, "viewEndIdx", viewEndIdx, "offsetIdx", l.offsetIdx, "offsetLine", l.offsetLine)
-
- for idx := range l.dirtyItems {
- if idx >= viewStartIdx && idx <= viewEndIdx {
- l.renderItem(idx)
- delete(l.dirtyItems, idx)
- }
- }
-
- lines := l.renderLines(viewStartIdx, viewEndIdx)
- for len(lines) < l.height {
- viewStartIdx--
- if viewStartIdx <= 0 {
- break
- }
-
- lines = l.renderLines(viewStartIdx, viewEndIdx)
- }
-
- if len(lines) > l.height {
- lines = lines[:l.height]
- }
-
- return strings.Join(lines, "\n")
-}
-
-// PrependItems prepends items to the list.
-func (l *List) PrependItems(items ...Item) {
- l.items = append(items, l.items...)
- // Shift existing item positions
- newItemPositions := make(map[int]itemPosition)
- for idx, pos := range l.itemPositions {
- newItemPositions[idx+len(items)] = pos
- }
- l.itemPositions = newItemPositions
-
- // Mark all items as dirty
- for idx := range l.items {
- l.dirtyItems[idx] = struct{}{}
- }
-
- // Adjust offset index
- l.offsetIdx += len(items)
-}
-
-// AppendItems appends items to the list.
-func (l *List) AppendItems(items ...Item) {
- l.items = append(l.items, items...)
- for idx := len(l.items) - len(items); idx < len(l.items); idx++ {
- l.dirtyItems[idx] = struct{}{}
- }
-}
-
-// Focus sets the focus state of the list.
-func (l *List) Focus() {
- l.focused = true
-}
-
-// Blur removes the focus state from the list.
-func (l *List) Blur() {
- l.focused = false
-}
-
-// ScrollToTop scrolls the list to the top.
-func (l *List) ScrollToTop() {
- l.offsetIdx = 0
- l.offsetLine = 0
-}
-
-// ScrollToBottom scrolls the list to the bottom.
-func (l *List) ScrollToBottom() {
- l.offsetIdx = len(l.items) - 1
- pos, ok := l.itemPositions[l.offsetIdx]
- if !ok {
- l.renderItem(l.offsetIdx)
- pos = l.itemPositions[l.offsetIdx]
- }
- l.offsetLine = l.height - pos.Height()
-}
-
-// ScrollToSelected scrolls the list to the selected item.
-func (l *List) ScrollToSelected() {
- if l.selectedIdx < 0 || l.selectedIdx >= len(l.items) {
- return
- }
- l.offsetIdx = l.selectedIdx
- l.offsetLine = 0
-}
-
-// SelectedItemInView returns whether the selected item is currently in view.
-func (l *List) SelectedItemInView() bool {
- if l.selectedIdx < 0 || l.selectedIdx >= len(l.items) {
- return false
- }
- startIdx, endIdx := l.findVisibleItems()
- return l.selectedIdx >= startIdx && l.selectedIdx <= endIdx
-}
-
-// SetSelected sets the selected item index in the list.
-func (l *List) SetSelected(index int) {
- if index < 0 || index >= len(l.items) {
- l.selectedIdx = -1
- } else {
- l.selectedIdx = index
- }
-}
-
-// SelectPrev selects the previous item in the list.
-func (l *List) SelectPrev() {
- if l.selectedIdx > 0 {
- l.selectedIdx--
- }
-}
-
-// SelectNext selects the next item in the list.
-func (l *List) SelectNext() {
- if l.selectedIdx < len(l.items)-1 {
- l.selectedIdx++
- }
-}
-
-// SelectFirst selects the first item in the list.
-func (l *List) SelectFirst() {
- if len(l.items) > 0 {
- l.selectedIdx = 0
- }
-}
-
-// SelectLast selects the last item in the list.
-func (l *List) SelectLast() {
- if len(l.items) > 0 {
- l.selectedIdx = len(l.items) - 1
- }
-}
-
-// SelectFirstInView selects the first item currently in view.
-func (l *List) SelectFirstInView() {
- startIdx, _ := l.findVisibleItems()
- l.selectedIdx = startIdx
-}
-
-// SelectLastInView selects the last item currently in view.
-func (l *List) SelectLastInView() {
- _, endIdx := l.findVisibleItems()
- l.selectedIdx = endIdx
-}
-
-// HandleMouseDown handles mouse down events at the given line in the viewport.
-func (l *List) HandleMouseDown(x, y int) {
-}
-
-// HandleMouseUp handles mouse up events at the given line in the viewport.
-func (l *List) HandleMouseUp(x, y int) {
-}
-
-// HandleMouseDrag handles mouse drag events at the given line in the viewport.
-func (l *List) HandleMouseDrag(x, y int) {
-}
-
-// countLines counts the number of lines in a string.
-func countLines(s string) int {
- if s == "" {
- return 0
- }
- return strings.Count(s, "\n") + 1
-}
@@ -2,7 +2,7 @@ package model
import (
"github.com/charmbracelet/crush/internal/ui/common"
- "github.com/charmbracelet/crush/internal/ui/lazylist"
+ "github.com/charmbracelet/crush/internal/ui/list"
uv "github.com/charmbracelet/ultraviolet"
)
@@ -10,13 +10,13 @@ import (
// messages.
type Chat struct {
com *common.Common
- list *lazylist.List
+ list *list.List
}
// NewChat creates a new instance of [Chat] that handles chat interactions and
// messages.
func NewChat(com *common.Common) *Chat {
- l := lazylist.NewList()
+ l := list.NewList()
l.SetGap(1)
return &Chat{
com: com,
@@ -45,14 +45,14 @@ func (m *Chat) Len() int {
}
// PrependItems prepends new items to the chat list.
-func (m *Chat) PrependItems(items ...lazylist.Item) {
+func (m *Chat) PrependItems(items ...list.Item) {
m.list.PrependItems(items...)
m.list.ScrollToIndex(0)
}
// AppendMessages appends a new message item to the chat list.
func (m *Chat) AppendMessages(msgs ...MessageItem) {
- items := make([]lazylist.Item, len(msgs))
+ items := make([]list.Item, len(msgs))
for i, msg := range msgs {
items[i] = msg
}
@@ -60,7 +60,7 @@ func (m *Chat) AppendMessages(msgs ...MessageItem) {
}
// AppendItems appends new items to the chat list.
-func (m *Chat) AppendItems(items ...lazylist.Item) {
+func (m *Chat) AppendItems(items ...list.Item) {
m.list.AppendItems(items...)
m.list.ScrollToIndex(m.list.Len() - 1)
}
@@ -12,7 +12,7 @@ import (
"github.com/charmbracelet/crush/internal/config"
"github.com/charmbracelet/crush/internal/message"
"github.com/charmbracelet/crush/internal/ui/common"
- "github.com/charmbracelet/crush/internal/ui/lazylist"
+ "github.com/charmbracelet/crush/internal/ui/list"
"github.com/charmbracelet/crush/internal/ui/styles"
"github.com/charmbracelet/crush/internal/ui/toolrender"
)
@@ -23,10 +23,10 @@ type Identifiable interface {
}
// MessageItem represents a [message.Message] item that can be displayed in the
-// UI and be part of a [lazylist.List] identifiable by a unique ID.
+// UI and be part of a [list.List] identifiable by a unique ID.
type MessageItem interface {
- lazylist.Item
- lazylist.Item
+ list.Item
+ list.Item
Identifiable
}
@@ -81,7 +81,7 @@ func (m *MessageContentItem) HighlightStyle() lipgloss.Style {
// Render renders the content at the given width, using cache if available.
//
-// It implements [lazylist.Item].
+// It implements [list.Item].
func (m *MessageContentItem) Render(width int) string {
contentWidth := width
// Cap width to maxWidth for markdown
@@ -163,7 +163,7 @@ func (t *ToolCallItem) HighlightStyle() lipgloss.Style {
return t.sty.TextSelection
}
-// Render implements lazylist.Item.
+// Render implements list.Item.
func (t *ToolCallItem) Render(width int) string {
// Render the tool call
ctx := &toolrender.RenderContext{
@@ -218,7 +218,7 @@ func (a *AttachmentItem) HighlightStyle() lipgloss.Style {
return a.sty.TextSelection
}
-// Render implements lazylist.Item.
+// Render implements list.Item.
func (a *AttachmentItem) Render(width int) string {
const maxFilenameWidth = 10
content := a.sty.Chat.Message.Attachment.Render(fmt.Sprintf(
@@ -275,7 +275,7 @@ func (t *ThinkingItem) HighlightStyle() lipgloss.Style {
return t.sty.TextSelection
}
-// Render implements lazylist.Item.
+// Render implements list.Item.
func (t *ThinkingItem) Render(width int) string {
cappedWidth := min(width, t.maxWidth)
@@ -353,7 +353,7 @@ func (s *SectionHeaderItem) BlurStyle() lipgloss.Style {
return s.sty.Chat.Message.AssistantBlurred
}
-// Render implements lazylist.Item.
+// Render implements list.Item.
func (s *SectionHeaderItem) Render(width int) string {
content := fmt.Sprintf("%s %s %s",
s.sty.Subtle.Render(styles.ModelIcon),