1package model
2
3import (
4 tea "charm.land/bubbletea/v2"
5 "github.com/charmbracelet/crush/internal/ui/anim"
6 "github.com/charmbracelet/crush/internal/ui/chat"
7 "github.com/charmbracelet/crush/internal/ui/common"
8 "github.com/charmbracelet/crush/internal/ui/list"
9 uv "github.com/charmbracelet/ultraviolet"
10 "github.com/charmbracelet/x/ansi"
11)
12
13// Chat represents the chat UI model that handles chat interactions and
14// messages.
15type Chat struct {
16 com *common.Common
17 list *list.List
18 idInxMap map[string]int // Map of message IDs to their indices in the list
19
20 // Mouse state
21 mouseDown bool
22 mouseDownItem int // Item index where mouse was pressed
23 mouseDownX int // X position in item content (character offset)
24 mouseDownY int // Y position in item (line offset)
25 mouseDragItem int // Current item index being dragged over
26 mouseDragX int // Current X in item content
27 mouseDragY int // Current Y in item
28}
29
30// NewChat creates a new instance of [Chat] that handles chat interactions and
31// messages.
32func NewChat(com *common.Common) *Chat {
33 c := &Chat{com: com, idInxMap: make(map[string]int)}
34 l := list.NewList()
35 l.SetGap(1)
36 l.RegisterRenderCallback(c.applyHighlightRange)
37 c.list = l
38 c.mouseDownItem = -1
39 c.mouseDragItem = -1
40 return c
41}
42
43// Height returns the height of the chat view port.
44func (m *Chat) Height() int {
45 return m.list.Height()
46}
47
48// Draw renders the chat UI component to the screen and the given area.
49func (m *Chat) Draw(scr uv.Screen, area uv.Rectangle) {
50 uv.NewStyledString(m.list.Render()).Draw(scr, area)
51}
52
53// SetSize sets the size of the chat view port.
54func (m *Chat) SetSize(width, height int) {
55 m.list.SetSize(width, height)
56}
57
58// Len returns the number of items in the chat list.
59func (m *Chat) Len() int {
60 return m.list.Len()
61}
62
63// SetMessages sets the chat messages to the provided list of message items.
64func (m *Chat) SetMessages(msgs ...chat.MessageItem) {
65 items := make([]list.Item, len(msgs))
66 for i, msg := range msgs {
67 m.idInxMap[msg.ID()] = i
68 items[i] = msg
69 }
70 m.list.SetItems(items...)
71 m.list.ScrollToBottom()
72}
73
74// AppendMessages appends a new message item to the chat list.
75func (m *Chat) AppendMessages(msgs ...chat.MessageItem) {
76 items := make([]list.Item, len(msgs))
77 indexOffset := len(m.idInxMap)
78 for i, msg := range msgs {
79 m.idInxMap[msg.ID()] = indexOffset + i
80 items[i] = msg
81 }
82 m.list.AppendItems(items...)
83}
84
85// Animate animated items in the chat list.
86func (m *Chat) Animate(msg anim.StepMsg) tea.Cmd {
87 item, ok := m.idInxMap[msg.ID]
88 // Item with the given ID exists
89 if !ok {
90 return nil
91 }
92 if animatable, ok := m.list.ItemAt(item).(chat.Animatable); ok {
93 return animatable.Animate(msg)
94 }
95 return nil
96}
97
98// Focus sets the focus state of the chat component.
99func (m *Chat) Focus() {
100 m.list.Focus()
101}
102
103// Blur removes the focus state from the chat component.
104func (m *Chat) Blur() {
105 m.list.Blur()
106}
107
108// ScrollToTop scrolls the chat view to the top.
109func (m *Chat) ScrollToTop() {
110 m.list.ScrollToTop()
111}
112
113// ScrollToBottom scrolls the chat view to the bottom.
114func (m *Chat) ScrollToBottom() {
115 m.list.ScrollToBottom()
116}
117
118// ScrollBy scrolls the chat view by the given number of line deltas.
119func (m *Chat) ScrollBy(lines int) {
120 m.list.ScrollBy(lines)
121}
122
123// ScrollToSelected scrolls the chat view to the selected item.
124func (m *Chat) ScrollToSelected() {
125 m.list.ScrollToSelected()
126}
127
128// SelectedItemInView returns whether the selected item is currently in view.
129func (m *Chat) SelectedItemInView() bool {
130 return m.list.SelectedItemInView()
131}
132
133// SetSelected sets the selected message index in the chat list.
134func (m *Chat) SetSelected(index int) {
135 m.list.SetSelected(index)
136}
137
138// SelectPrev selects the previous message in the chat list.
139func (m *Chat) SelectPrev() {
140 m.list.SelectPrev()
141}
142
143// SelectNext selects the next message in the chat list.
144func (m *Chat) SelectNext() {
145 m.list.SelectNext()
146}
147
148// SelectFirst selects the first message in the chat list.
149func (m *Chat) SelectFirst() {
150 m.list.SelectFirst()
151}
152
153// SelectLast selects the last message in the chat list.
154func (m *Chat) SelectLast() {
155 m.list.SelectLast()
156}
157
158// SelectFirstInView selects the first message currently in view.
159func (m *Chat) SelectFirstInView() {
160 m.list.SelectFirstInView()
161}
162
163// SelectLastInView selects the last message currently in view.
164func (m *Chat) SelectLastInView() {
165 m.list.SelectLastInView()
166}
167
168// GetMessageItem returns the message item at the given id.
169func (m *Chat) GetMessageItem(id string) chat.MessageItem {
170 idx, ok := m.idInxMap[id]
171 if !ok {
172 return nil
173 }
174 item, ok := m.list.ItemAt(idx).(chat.MessageItem)
175 if !ok {
176 return nil
177 }
178 return item
179}
180
181// HandleMouseDown handles mouse down events for the chat component.
182func (m *Chat) HandleMouseDown(x, y int) bool {
183 if m.list.Len() == 0 {
184 return false
185 }
186
187 itemIdx, itemY := m.list.ItemIndexAtPosition(x, y)
188 if itemIdx < 0 {
189 return false
190 }
191
192 m.mouseDown = true
193 m.mouseDownItem = itemIdx
194 m.mouseDownX = x
195 m.mouseDownY = itemY
196 m.mouseDragItem = itemIdx
197 m.mouseDragX = x
198 m.mouseDragY = itemY
199
200 // Select the item that was clicked
201 m.list.SetSelected(itemIdx)
202
203 if clickable, ok := m.list.SelectedItem().(list.MouseClickable); ok {
204 return clickable.HandleMouseClick(ansi.MouseButton1, x, itemY)
205 }
206
207 return true
208}
209
210// HandleMouseUp handles mouse up events for the chat component.
211func (m *Chat) HandleMouseUp(x, y int) bool {
212 if !m.mouseDown {
213 return false
214 }
215
216 // TODO: Handle the behavior when mouse is released after a drag selection
217 // (e.g., copy selected text to clipboard)
218
219 m.mouseDown = false
220 return true
221}
222
223// HandleMouseDrag handles mouse drag events for the chat component.
224func (m *Chat) HandleMouseDrag(x, y int) bool {
225 if !m.mouseDown {
226 return false
227 }
228
229 if m.list.Len() == 0 {
230 return false
231 }
232
233 itemIdx, itemY := m.list.ItemIndexAtPosition(x, y)
234 if itemIdx < 0 {
235 return false
236 }
237
238 m.mouseDragItem = itemIdx
239 m.mouseDragX = x
240 m.mouseDragY = itemY
241
242 return true
243}
244
245// ClearMouse clears the current mouse interaction state.
246func (m *Chat) ClearMouse() {
247 m.mouseDown = false
248 m.mouseDownItem = -1
249 m.mouseDragItem = -1
250}
251
252// applyHighlightRange applies the current highlight range to the chat items.
253func (m *Chat) applyHighlightRange(idx, selectedIdx int, item list.Item) list.Item {
254 if hi, ok := item.(list.Highlightable); ok {
255 // Apply highlight
256 startItemIdx, startLine, startCol, endItemIdx, endLine, endCol := m.getHighlightRange()
257 sLine, sCol, eLine, eCol := -1, -1, -1, -1
258 if idx >= startItemIdx && idx <= endItemIdx {
259 if idx == startItemIdx && idx == endItemIdx {
260 // Single item selection
261 sLine = startLine
262 sCol = startCol
263 eLine = endLine
264 eCol = endCol
265 } else if idx == startItemIdx {
266 // First item - from start position to end of item
267 sLine = startLine
268 sCol = startCol
269 eLine = -1
270 eCol = -1
271 } else if idx == endItemIdx {
272 // Last item - from start of item to end position
273 sLine = 0
274 sCol = 0
275 eLine = endLine
276 eCol = endCol
277 } else {
278 // Middle item - fully highlighted
279 sLine = 0
280 sCol = 0
281 eLine = -1
282 eCol = -1
283 }
284 }
285
286 hi.Highlight(sLine, sCol, eLine, eCol)
287 return hi.(list.Item)
288 }
289
290 return item
291}
292
293// getHighlightRange returns the current highlight range.
294func (m *Chat) getHighlightRange() (startItemIdx, startLine, startCol, endItemIdx, endLine, endCol int) {
295 if m.mouseDownItem < 0 {
296 return -1, -1, -1, -1, -1, -1
297 }
298
299 downItemIdx := m.mouseDownItem
300 dragItemIdx := m.mouseDragItem
301
302 // Determine selection direction
303 draggingDown := dragItemIdx > downItemIdx ||
304 (dragItemIdx == downItemIdx && m.mouseDragY > m.mouseDownY) ||
305 (dragItemIdx == downItemIdx && m.mouseDragY == m.mouseDownY && m.mouseDragX >= m.mouseDownX)
306
307 if draggingDown {
308 // Normal forward selection
309 startItemIdx = downItemIdx
310 startLine = m.mouseDownY
311 startCol = m.mouseDownX
312 endItemIdx = dragItemIdx
313 endLine = m.mouseDragY
314 endCol = m.mouseDragX
315 } else {
316 // Backward selection (dragging up)
317 startItemIdx = dragItemIdx
318 startLine = m.mouseDragY
319 startCol = m.mouseDragX
320 endItemIdx = downItemIdx
321 endLine = m.mouseDownY
322 endCol = m.mouseDownX
323 }
324
325 return startItemIdx, startLine, startCol, endItemIdx, endLine, endCol
326}