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