chat.go

  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}