on main help sign in
refactor(ui): rename lazylist package to list and update imports
b25730dAyman Bagabas
internal/ui/lazylist/list.go.bakdeleted−413
@@ -1,413 +0,0 @@
1−package lazylist
2−
3−import (
4− "log/slog"
5− "strings"
6−)
7−
8−// List represents a list of items that can be lazily rendered. A list is
9−// always rendered like a chat conversation where items are stacked vertically
10−// from top to bottom.
11−type List struct {
12− // Viewport size
13− width, height int
14−
15− // Items in the list
16− items []Item
17−
18− // Gap between items (0 or less means no gap)
19− gap int
20−
21− // Focus and selection state
22− focused bool
23− selectedIdx int // The current selected index -1 means no selection
24−
25− // Item positioning. If a position exists in the map, it means the item has
26− // been rendered and measured.
27− itemPositions map[int]itemPosition
28−
29− // Rendered content and cache
30− lines []string
31− renderedItems map[int]renderedItem
32− offsetIdx int // Index of the first visible item in the viewport
33− offsetLine int // The offset line from the start of the offsetIdx item (can be negative)
34−
35− // Dirty tracking
36− dirtyItems map[int]struct{}
37−}
38−
39−// renderedItem holds the rendered content and height of an item.
40−type renderedItem struct {
41− content string
42− height int
43−}
44−
45−// itemPosition holds the start and end line of an item in the list.
46−type itemPosition struct {
47− startLine int
48− endLine int
49−}
50−
51−// Height returns the height of item based on its start and end lines.
52−func (ip itemPosition) Height() int {
53− return ip.endLine - ip.startLine
54−}
55−
56−// NewList creates a new lazy-loaded list.
57−func NewList(items ...Item) *List {
58− l := new(List)
59− l.items = items
60− l.itemPositions = make(map[int]itemPosition)
61− l.renderedItems = make(map[int]renderedItem)
62− l.dirtyItems = make(map[int]struct{})
63− return l
64−}
65−
66−// SetSize sets the size of the list viewport.
67−func (l *List) SetSize(width, height int) {
68− if width != l.width {
69− // Mark all rendered items as dirty if width changes because their
70− // layout may change.
71− for idx := range l.itemPositions {
72− l.dirtyItems[idx] = struct{}{}
73− }
74− }
75− l.width = width
76− l.height = height
77−}
78−
79−// SetGap sets the gap between items.
80−func (l *List) SetGap(gap int) {
81− l.gap = gap
82−}
83−
84−// Width returns the width of the list viewport.
85−func (l *List) Width() int {
86− return l.width
87−}
88−
89−// Height returns the height of the list viewport.
90−func (l *List) Height() int {
91− return l.height
92−}
93−
94−// Len returns the number of items in the list.
95−func (l *List) Len() int {
96− return len(l.items)
97−}
98−
99−// renderItem renders the item at the given index and updates its cache and
100−// position.
101−func (l *List) renderItem(idx int) {
102− if idx < 0 || idx >= len(l.items) {
103− return
104− }
105−
106− item := l.items[idx]
107− rendered := item.Render(l.width)
108− height := countLines(rendered)
109−
110− l.renderedItems[idx] = renderedItem{
111− content: rendered,
112− height: height,
113− }
114−
115− // Calculate item position
116− var startLine int
117− if idx == 0 {
118− startLine = 0
119− } else {
120− prevPos, ok := l.itemPositions[idx-1]
121− if !ok {
122− l.renderItem(idx - 1)
123− prevPos = l.itemPositions[idx-1]
124− }
125− startLine = prevPos.endLine
126− if l.gap > 0 {
127− startLine += l.gap
128− }
129− }
130− endLine := startLine + height
131−
132− l.itemPositions[idx] = itemPosition{
133− startLine: startLine,
134− endLine: endLine,
135− }
136−}
137−
138−// ScrollToIndex scrolls the list to the given item index.
139−func (l *List) ScrollToIndex(index int) {
140− if index < 0 || index >= len(l.items) {
141− return
142− }
143− l.offsetIdx = index
144− l.offsetLine = 0
145−}
146−
147−// ScrollBy scrolls the list by the given number of lines.
148−func (l *List) ScrollBy(lines int) {
149− l.offsetLine += lines
150− if l.offsetIdx <= 0 && l.offsetLine < 0 {
151− l.offsetIdx = 0
152− l.offsetLine = 0
153− return
154− }
155−
156− // Adjust offset index and line if needed
157− for l.offsetLine < 0 && l.offsetIdx > 0 {
158− // Move up to previous item
159− l.offsetIdx--
160− prevPos, ok := l.itemPositions[l.offsetIdx]
161− if !ok {
162− l.renderItem(l.offsetIdx)
163− prevPos = l.itemPositions[l.offsetIdx]
164− }
165− l.offsetLine += prevPos.Height()
166− if l.gap > 0 {
167− l.offsetLine += l.gap
168− }
169− }
170−
171− for {
172− currentPos, ok := l.itemPositions[l.offsetIdx]
173− if !ok {
174− l.renderItem(l.offsetIdx)
175− currentPos = l.itemPositions[l.offsetIdx]
176− }
177− if l.offsetLine >= currentPos.Height() {
178− // Move down to next item
179− l.offsetLine -= currentPos.Height()
180− if l.gap > 0 {
181− l.offsetLine -= l.gap
182− }
183− l.offsetIdx++
184− if l.offsetIdx >= len(l.items) {
185− l.offsetIdx = len(l.items) - 1
186− l.offsetLine = currentPos.Height() - 1
187− break
188− }
189− } else {
190− break
191− }
192− }
193−}
194−
195−// findVisibleItems finds the range of items that are visible in the viewport.
196−func (l *List) findVisibleItems() (startIdx, endIdx int) {
197− startIdx = l.offsetIdx
198− endIdx = startIdx + 1
199−
200− // Render items until we fill the viewport
201− visibleHeight := -l.offsetLine
202− for endIdx < len(l.items) {
203− pos, ok := l.itemPositions[endIdx-1]
204− if !ok {
205− l.renderItem(endIdx - 1)
206− pos = l.itemPositions[endIdx-1]
207− }
208− visibleHeight += pos.Height()
209− if endIdx-1 < len(l.items)-1 && l.gap > 0 {
210− visibleHeight += l.gap
211− }
212− if visibleHeight >= l.height {
213− break
214− }
215− endIdx++
216− }
217−
218− if endIdx > len(l.items)-1 {
219− endIdx = len(l.items) - 1
220− }
221−
222− return startIdx, endIdx
223−}
224−
225−// renderLines renders the items between startIdx and endIdx into lines.
226−func (l *List) renderLines(startIdx, endIdx int) []string {
227− var lines []string
228− for idx := startIdx; idx < endIdx+1; idx++ {
229− rendered, ok := l.renderedItems[idx]
230− if !ok {
231− l.renderItem(idx)
232− rendered = l.renderedItems[idx]
233− }
234− itemLines := strings.Split(rendered.content, "\n")
235− lines = append(lines, itemLines...)
236− if l.gap > 0 && idx < endIdx {
237− for i := 0; i < l.gap; i++ {
238− lines = append(lines, "")
239− }
240− }
241− }
242− return lines
243−}
244−
245−// Render renders the list and returns the visible lines.
246−func (l *List) Render() string {
247− viewStartIdx, viewEndIdx := l.findVisibleItems()
248− slog.Info("Render", "viewStartIdx", viewStartIdx, "viewEndIdx", viewEndIdx, "offsetIdx", l.offsetIdx, "offsetLine", l.offsetLine)
249−
250− for idx := range l.dirtyItems {
251− if idx >= viewStartIdx && idx <= viewEndIdx {
252− l.renderItem(idx)
253− delete(l.dirtyItems, idx)
254− }
255− }
256−
257− lines := l.renderLines(viewStartIdx, viewEndIdx)
258− for len(lines) < l.height {
259− viewStartIdx--
260− if viewStartIdx <= 0 {
261− break
262− }
263−
264− lines = l.renderLines(viewStartIdx, viewEndIdx)
265− }
266−
267− if len(lines) > l.height {
268− lines = lines[:l.height]
269− }
270−
271− return strings.Join(lines, "\n")
272−}
273−
274−// PrependItems prepends items to the list.
275−func (l *List) PrependItems(items ...Item) {
276− l.items = append(items, l.items...)
277− // Shift existing item positions
278− newItemPositions := make(map[int]itemPosition)
279− for idx, pos := range l.itemPositions {
280− newItemPositions[idx+len(items)] = pos
281− }
282− l.itemPositions = newItemPositions
283−
284− // Mark all items as dirty
285− for idx := range l.items {
286− l.dirtyItems[idx] = struct{}{}
287− }
288−
289− // Adjust offset index
290− l.offsetIdx += len(items)
291−}
292−
293−// AppendItems appends items to the list.
294−func (l *List) AppendItems(items ...Item) {
295− l.items = append(l.items, items...)
296− for idx := len(l.items) - len(items); idx < len(l.items); idx++ {
297− l.dirtyItems[idx] = struct{}{}
298− }
299−}
300−
301−// Focus sets the focus state of the list.
302−func (l *List) Focus() {
303− l.focused = true
304−}
305−
306−// Blur removes the focus state from the list.
307−func (l *List) Blur() {
308− l.focused = false
309−}
310−
311−// ScrollToTop scrolls the list to the top.
312−func (l *List) ScrollToTop() {
313− l.offsetIdx = 0
314− l.offsetLine = 0
315−}
316−
317−// ScrollToBottom scrolls the list to the bottom.
318−func (l *List) ScrollToBottom() {
319− l.offsetIdx = len(l.items) - 1
320− pos, ok := l.itemPositions[l.offsetIdx]
321− if !ok {
322− l.renderItem(l.offsetIdx)
323− pos = l.itemPositions[l.offsetIdx]
324− }
325− l.offsetLine = l.height - pos.Height()
326−}
327−
328−// ScrollToSelected scrolls the list to the selected item.
329−func (l *List) ScrollToSelected() {
330− if l.selectedIdx < 0 || l.selectedIdx >= len(l.items) {
331− return
332− }
333− l.offsetIdx = l.selectedIdx
334− l.offsetLine = 0
335−}
336−
337−// SelectedItemInView returns whether the selected item is currently in view.
338−func (l *List) SelectedItemInView() bool {
339− if l.selectedIdx < 0 || l.selectedIdx >= len(l.items) {
340− return false
341− }
342− startIdx, endIdx := l.findVisibleItems()
343− return l.selectedIdx >= startIdx && l.selectedIdx <= endIdx
344−}
345−
346−// SetSelected sets the selected item index in the list.
347−func (l *List) SetSelected(index int) {
348− if index < 0 || index >= len(l.items) {
349− l.selectedIdx = -1
350− } else {
351− l.selectedIdx = index
352− }
353−}
354−
355−// SelectPrev selects the previous item in the list.
356−func (l *List) SelectPrev() {
357− if l.selectedIdx > 0 {
358− l.selectedIdx--
359− }
360−}
361−
362−// SelectNext selects the next item in the list.
363−func (l *List) SelectNext() {
364− if l.selectedIdx < len(l.items)-1 {
365− l.selectedIdx++
366− }
367−}
368−
369−// SelectFirst selects the first item in the list.
370−func (l *List) SelectFirst() {
371− if len(l.items) > 0 {
372− l.selectedIdx = 0
373− }
374−}
375−
376−// SelectLast selects the last item in the list.
377−func (l *List) SelectLast() {
378− if len(l.items) > 0 {
379− l.selectedIdx = len(l.items) - 1
380− }
381−}
382−
383−// SelectFirstInView selects the first item currently in view.
384−func (l *List) SelectFirstInView() {
385− startIdx, _ := l.findVisibleItems()
386− l.selectedIdx = startIdx
387−}
388−
389−// SelectLastInView selects the last item currently in view.
390−func (l *List) SelectLastInView() {
391− _, endIdx := l.findVisibleItems()
392− l.selectedIdx = endIdx
393−}
394−
395−// HandleMouseDown handles mouse down events at the given line in the viewport.
396−func (l *List) HandleMouseDown(x, y int) {
397−}
398−
399−// HandleMouseUp handles mouse up events at the given line in the viewport.
400−func (l *List) HandleMouseUp(x, y int) {
401−}
402−
403−// HandleMouseDrag handles mouse drag events at the given line in the viewport.
404−func (l *List) HandleMouseDrag(x, y int) {
405−}
406−
407−// countLines counts the number of lines in a string.
408−func countLines(s string) int {
409− if s == "" {
410− return 0
411− }
412− return strings.Count(s, "\n") + 1
413−}
internal/ui/lazylist/highlight.go → internal/ui/list/highlight.gorenamed+1−1
@@ -1,4 +1,4 @@
1−package lazylist
1+package list
2
2
3import (
3import (
4 "image"
4 "image"
internal/ui/lazylist/item.go → internal/ui/list/item.gorenamed+1−1
@@ -1,4 +1,4 @@
1−package lazylist
1+package list
2
2
3import (
3import (
4 "charm.land/lipgloss/v2"
4 "charm.land/lipgloss/v2"
internal/ui/lazylist/list.go → internal/ui/list/list.gorenamed+1−1
@@ -1,4 +1,4 @@
1−package lazylist
1+package list
2
2
3import (
3import (
4 "image"
4 "image"
internal/ui/model/chat.go+6−6
@@ -2,7 +2,7 @@ package model
2
2
3import (
3import (
4 "github.com/charmbracelet/crush/internal/ui/common"
4 "github.com/charmbracelet/crush/internal/ui/common"
5− "github.com/charmbracelet/crush/internal/ui/lazylist"
5+ "github.com/charmbracelet/crush/internal/ui/list"
6 uv "github.com/charmbracelet/ultraviolet"
6 uv "github.com/charmbracelet/ultraviolet"
7)
7)
8
8
@@ -10,13 +10,13 @@ import (
10// messages.
10// messages.
11type Chat struct {
11type Chat struct {
12 com *common.Common
12 com *common.Common
13− list *lazylist.List
13+ list *list.List
14}
14}
15
15
16// NewChat creates a new instance of [Chat] that handles chat interactions and
16// NewChat creates a new instance of [Chat] that handles chat interactions and
17// messages.
17// messages.
18func NewChat(com *common.Common) *Chat {
18func NewChat(com *common.Common) *Chat {
19− l := lazylist.NewList()
19+ l := list.NewList()
20 l.SetGap(1)
20 l.SetGap(1)
21 return &Chat{
21 return &Chat{
22 com: com,
22 com: com,
@@ -45,14 +45,14 @@ func (m *Chat) Len() int {
45}
45}
46
46
47// PrependItems prepends new items to the chat list.
47// PrependItems prepends new items to the chat list.
48−func (m *Chat) PrependItems(items ...lazylist.Item) {
48+func (m *Chat) PrependItems(items ...list.Item) {
49 m.list.PrependItems(items...)
49 m.list.PrependItems(items...)
50 m.list.ScrollToIndex(0)
50 m.list.ScrollToIndex(0)
51}
51}
52
52
53// AppendMessages appends a new message item to the chat list.
53// AppendMessages appends a new message item to the chat list.
54func (m *Chat) AppendMessages(msgs ...MessageItem) {
54func (m *Chat) AppendMessages(msgs ...MessageItem) {
55− items := make([]lazylist.Item, len(msgs))
55+ items := make([]list.Item, len(msgs))
56 for i, msg := range msgs {
56 for i, msg := range msgs {
57 items[i] = msg
57 items[i] = msg
58 }
58 }
@@ -60,7 +60,7 @@ func (m *Chat) AppendMessages(msgs ...MessageItem) {
60}
60}
61
61
62// AppendItems appends new items to the chat list.
62// AppendItems appends new items to the chat list.
63−func (m *Chat) AppendItems(items ...lazylist.Item) {
63+func (m *Chat) AppendItems(items ...list.Item) {
64 m.list.AppendItems(items...)
64 m.list.AppendItems(items...)
65 m.list.ScrollToIndex(m.list.Len() - 1)
65 m.list.ScrollToIndex(m.list.Len() - 1)
66}
66}
internal/ui/model/items.go+9−9
@@ -12,7 +12,7 @@ import (
12 "github.com/charmbracelet/crush/internal/config"
12 "github.com/charmbracelet/crush/internal/config"
13 "github.com/charmbracelet/crush/internal/message"
13 "github.com/charmbracelet/crush/internal/message"
14 "github.com/charmbracelet/crush/internal/ui/common"
14 "github.com/charmbracelet/crush/internal/ui/common"
15− "github.com/charmbracelet/crush/internal/ui/lazylist"
15+ "github.com/charmbracelet/crush/internal/ui/list"
16 "github.com/charmbracelet/crush/internal/ui/styles"
16 "github.com/charmbracelet/crush/internal/ui/styles"
17 "github.com/charmbracelet/crush/internal/ui/toolrender"
17 "github.com/charmbracelet/crush/internal/ui/toolrender"
18)
18)
@@ -23,10 +23,10 @@ type Identifiable interface {
23}
23}
24
24
25// MessageItem represents a [message.Message] item that can be displayed in the
25// MessageItem represents a [message.Message] item that can be displayed in the
26−// UI and be part of a [lazylist.List] identifiable by a unique ID.
26+// UI and be part of a [list.List] identifiable by a unique ID.
27type MessageItem interface {
27type MessageItem interface {
28− lazylist.Item
28+ list.Item
29− lazylist.Item
29+ list.Item
30 Identifiable
30 Identifiable
31}
31}
32
32
@@ -81,7 +81,7 @@ func (m *MessageContentItem) HighlightStyle() lipgloss.Style {
81
81
82// Render renders the content at the given width, using cache if available.
82// Render renders the content at the given width, using cache if available.
83//
83//
84−// It implements [lazylist.Item].
84+// It implements [list.Item].
85func (m *MessageContentItem) Render(width int) string {
85func (m *MessageContentItem) Render(width int) string {
86 contentWidth := width
86 contentWidth := width
87 // Cap width to maxWidth for markdown
87 // Cap width to maxWidth for markdown
@@ -163,7 +163,7 @@ func (t *ToolCallItem) HighlightStyle() lipgloss.Style {
163 return t.sty.TextSelection
163 return t.sty.TextSelection
164}
164}
165
165
166−// Render implements lazylist.Item.
166+// Render implements list.Item.
167func (t *ToolCallItem) Render(width int) string {
167func (t *ToolCallItem) Render(width int) string {
168 // Render the tool call
168 // Render the tool call
169 ctx := &toolrender.RenderContext{
169 ctx := &toolrender.RenderContext{
@@ -218,7 +218,7 @@ func (a *AttachmentItem) HighlightStyle() lipgloss.Style {
218 return a.sty.TextSelection
218 return a.sty.TextSelection
219}
219}
220
220
221−// Render implements lazylist.Item.
221+// Render implements list.Item.
222func (a *AttachmentItem) Render(width int) string {
222func (a *AttachmentItem) Render(width int) string {
223 const maxFilenameWidth = 10
223 const maxFilenameWidth = 10
224 content := a.sty.Chat.Message.Attachment.Render(fmt.Sprintf(
224 content := a.sty.Chat.Message.Attachment.Render(fmt.Sprintf(
@@ -275,7 +275,7 @@ func (t *ThinkingItem) HighlightStyle() lipgloss.Style {
275 return t.sty.TextSelection
275 return t.sty.TextSelection
276}
276}
277
277
278−// Render implements lazylist.Item.
278+// Render implements list.Item.
279func (t *ThinkingItem) Render(width int) string {
279func (t *ThinkingItem) Render(width int) string {
280 cappedWidth := min(width, t.maxWidth)
280 cappedWidth := min(width, t.maxWidth)
281
281
@@ -353,7 +353,7 @@ func (s *SectionHeaderItem) BlurStyle() lipgloss.Style {
353 return s.sty.Chat.Message.AssistantBlurred
353 return s.sty.Chat.Message.AssistantBlurred
354}
354}
355
355
356−// Render implements lazylist.Item.
356+// Render implements list.Item.
357func (s *SectionHeaderItem) Render(width int) string {
357func (s *SectionHeaderItem) Render(width int) string {
358 content := fmt.Sprintf("%s %s %s",
358 content := fmt.Sprintf("%s %s %s",
359 s.sty.Subtle.Render(styles.ModelIcon),
359 s.sty.Subtle.Render(styles.ModelIcon),