chat.go

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