1package chat
2
3import (
4 "context"
5 "time"
6
7 "github.com/atotto/clipboard"
8 "github.com/charmbracelet/bubbles/v2/key"
9 tea "github.com/charmbracelet/bubbletea/v2"
10 "github.com/charmbracelet/crush/internal/app"
11 "github.com/charmbracelet/crush/internal/llm/agent"
12 "github.com/charmbracelet/crush/internal/message"
13 "github.com/charmbracelet/crush/internal/permission"
14 "github.com/charmbracelet/crush/internal/pubsub"
15 "github.com/charmbracelet/crush/internal/session"
16 "github.com/charmbracelet/crush/internal/tui/components/chat/messages"
17 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
18 "github.com/charmbracelet/crush/internal/tui/exp/list"
19 "github.com/charmbracelet/crush/internal/tui/styles"
20 "github.com/charmbracelet/crush/internal/tui/util"
21)
22
23type SendMsg struct {
24 Text string
25 Attachments []message.Attachment
26}
27
28type SessionSelectedMsg = session.Session
29
30type SessionClearedMsg struct{}
31
32const (
33 NotFound = -1
34)
35
36// MessageListCmp represents a component that displays a list of chat messages
37// with support for real-time updates and session management.
38type MessageListCmp interface {
39 util.Model
40 layout.Sizeable
41 layout.Focusable
42 layout.Help
43
44 SetSession(session.Session) tea.Cmd
45 GoToBottom() tea.Cmd
46 GetSelectedText() string
47 CopySelectedText(bool) tea.Cmd
48}
49
50// messageListCmp implements MessageListCmp, providing a virtualized list
51// of chat messages with support for tool calls, real-time updates, and
52// session switching.
53type messageListCmp struct {
54 app *app.App
55 width, height int
56 session session.Session
57 listCmp list.List[list.Item]
58 previousSelected string // Last selected item index for restoring focus
59
60 lastUserMessageTime int64
61 defaultListKeyMap list.KeyMap
62
63 // Click tracking for double/triple click detection
64 lastClickTime time.Time
65 lastClickX int
66 lastClickY int
67 clickCount int
68}
69
70// New creates a new message list component with custom keybindings
71// and reverse ordering (newest messages at bottom).
72func New(app *app.App) MessageListCmp {
73 defaultListKeyMap := list.DefaultKeyMap()
74 listCmp := list.New(
75 []list.Item{},
76 list.WithGap(1),
77 list.WithDirectionBackward(),
78 list.WithFocus(false),
79 list.WithKeyMap(defaultListKeyMap),
80 list.WithEnableMouse(),
81 )
82 return &messageListCmp{
83 app: app,
84 listCmp: listCmp,
85 previousSelected: "",
86 defaultListKeyMap: defaultListKeyMap,
87 }
88}
89
90// Init initializes the component.
91func (m *messageListCmp) Init() tea.Cmd {
92 return m.listCmp.Init()
93}
94
95// Update handles incoming messages and updates the component state.
96func (m *messageListCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
97 switch msg := msg.(type) {
98 case tea.KeyPressMsg:
99 if key.Matches(msg, messages.CopyKey) && m.listCmp.HasSelection() {
100 return m, m.CopySelectedText(true)
101 }
102 case tea.MouseClickMsg:
103 x := msg.X - 1 // Adjust for padding
104 y := msg.Y - 1 // Adjust for padding
105 if x < 0 || y < 0 || x >= m.width-2 || y >= m.height-1 {
106 return m, nil // Ignore clicks outside the component
107 }
108 if msg.Button == tea.MouseLeft {
109 return m, m.handleMouseClick(x, y)
110 }
111 return m, nil
112 case tea.MouseMotionMsg:
113 x := msg.X - 1 // Adjust for padding
114 y := msg.Y - 1 // Adjust for padding
115 if x < 0 || y < 0 || x >= m.width-2 || y >= m.height-1 {
116 if y < 0 {
117 return m, m.listCmp.MoveUp(1)
118 }
119 if y >= m.height-1 {
120 return m, m.listCmp.MoveDown(1)
121 }
122 return m, nil // Ignore clicks outside the component
123 }
124 if msg.Button == tea.MouseLeft {
125 m.listCmp.EndSelection(x, y)
126 }
127 return m, nil
128 case tea.MouseReleaseMsg:
129 x := msg.X - 1 // Adjust for padding
130 y := msg.Y - 1 // Adjust for padding
131 if msg.Button == tea.MouseLeft {
132 if x < 0 || y < 0 || x >= m.width-2 || y >= m.height-1 {
133 m.listCmp.SelectionStop()
134 } else {
135 m.listCmp.EndSelection(x, y)
136 m.listCmp.SelectionStop()
137 }
138 }
139 return m, nil
140 case pubsub.Event[permission.PermissionNotification]:
141 return m, m.handlePermissionRequest(msg.Payload)
142 case SessionSelectedMsg:
143 if msg.ID != m.session.ID {
144 cmd := m.SetSession(msg)
145 return m, cmd
146 }
147 return m, nil
148 case SessionClearedMsg:
149 m.session = session.Session{}
150 return m, m.listCmp.SetItems([]list.Item{})
151
152 case pubsub.Event[message.Message]:
153 cmd := m.handleMessageEvent(msg)
154 return m, cmd
155
156 case tea.MouseWheelMsg:
157 u, cmd := m.listCmp.Update(msg)
158 m.listCmp = u.(list.List[list.Item])
159 return m, cmd
160 }
161
162 u, cmd := m.listCmp.Update(msg)
163 m.listCmp = u.(list.List[list.Item])
164 return m, cmd
165}
166
167// View renders the message list or an initial screen if empty.
168func (m *messageListCmp) View() string {
169 t := styles.CurrentTheme()
170 return t.S().Base.
171 Padding(1, 1, 0, 1).
172 Width(m.width).
173 Height(m.height).
174 Render(
175 m.listCmp.View(),
176 )
177}
178
179func (m *messageListCmp) handlePermissionRequest(permission permission.PermissionNotification) tea.Cmd {
180 items := m.listCmp.Items()
181 if toolCallIndex := m.findToolCallByID(items, permission.ToolCallID); toolCallIndex != NotFound {
182 toolCall := items[toolCallIndex].(messages.ToolCallCmp)
183 toolCall.SetPermissionRequested()
184 if permission.Granted {
185 toolCall.SetPermissionGranted()
186 }
187 m.listCmp.UpdateItem(toolCall.ID(), toolCall)
188 }
189 return nil
190}
191
192// handleChildSession handles messages from child sessions (agent tools).
193func (m *messageListCmp) handleChildSession(event pubsub.Event[message.Message]) tea.Cmd {
194 var cmds []tea.Cmd
195 if len(event.Payload.ToolCalls()) == 0 && len(event.Payload.ToolResults()) == 0 {
196 return nil
197 }
198 items := m.listCmp.Items()
199 toolCallInx := NotFound
200 var toolCall messages.ToolCallCmp
201 for i := len(items) - 1; i >= 0; i-- {
202 if msg, ok := items[i].(messages.ToolCallCmp); ok {
203 if msg.GetToolCall().ID == event.Payload.SessionID {
204 toolCallInx = i
205 toolCall = msg
206 }
207 }
208 }
209 if toolCallInx == NotFound {
210 return nil
211 }
212 nestedToolCalls := toolCall.GetNestedToolCalls()
213 for _, tc := range event.Payload.ToolCalls() {
214 found := false
215 for existingInx, existingTC := range nestedToolCalls {
216 if existingTC.GetToolCall().ID == tc.ID {
217 nestedToolCalls[existingInx].SetToolCall(tc)
218 found = true
219 break
220 }
221 }
222 if !found {
223 nestedCall := messages.NewToolCallCmp(
224 event.Payload.ID,
225 tc,
226 m.app.Permissions,
227 messages.WithToolCallNested(true),
228 )
229 cmds = append(cmds, nestedCall.Init())
230 nestedToolCalls = append(
231 nestedToolCalls,
232 nestedCall,
233 )
234 }
235 }
236 for _, tr := range event.Payload.ToolResults() {
237 for nestedInx, nestedTC := range nestedToolCalls {
238 if nestedTC.GetToolCall().ID == tr.ToolCallID {
239 nestedToolCalls[nestedInx].SetToolResult(tr)
240 break
241 }
242 }
243 }
244
245 toolCall.SetNestedToolCalls(nestedToolCalls)
246 m.listCmp.UpdateItem(
247 toolCall.ID(),
248 toolCall,
249 )
250 return tea.Batch(cmds...)
251}
252
253// handleMessageEvent processes different types of message events (created/updated).
254func (m *messageListCmp) handleMessageEvent(event pubsub.Event[message.Message]) tea.Cmd {
255 switch event.Type {
256 case pubsub.CreatedEvent:
257 if event.Payload.SessionID != m.session.ID {
258 return m.handleChildSession(event)
259 }
260 if m.messageExists(event.Payload.ID) {
261 return nil
262 }
263 return m.handleNewMessage(event.Payload)
264 case pubsub.UpdatedEvent:
265 if event.Payload.SessionID != m.session.ID {
266 return m.handleChildSession(event)
267 }
268 switch event.Payload.Role {
269 case message.Assistant:
270 return m.handleUpdateAssistantMessage(event.Payload)
271 case message.Tool:
272 return m.handleToolMessage(event.Payload)
273 }
274 }
275 return nil
276}
277
278// messageExists checks if a message with the given ID already exists in the list.
279func (m *messageListCmp) messageExists(messageID string) bool {
280 items := m.listCmp.Items()
281 // Search backwards as new messages are more likely to be at the end
282 for i := len(items) - 1; i >= 0; i-- {
283 if msg, ok := items[i].(messages.MessageCmp); ok && msg.GetMessage().ID == messageID {
284 return true
285 }
286 }
287 return false
288}
289
290// handleNewMessage routes new messages to appropriate handlers based on role.
291func (m *messageListCmp) handleNewMessage(msg message.Message) tea.Cmd {
292 switch msg.Role {
293 case message.User:
294 return m.handleNewUserMessage(msg)
295 case message.Assistant:
296 return m.handleNewAssistantMessage(msg)
297 case message.Tool:
298 return m.handleToolMessage(msg)
299 }
300 return nil
301}
302
303// handleNewUserMessage adds a new user message to the list and updates the timestamp.
304func (m *messageListCmp) handleNewUserMessage(msg message.Message) tea.Cmd {
305 m.lastUserMessageTime = msg.CreatedAt
306 return m.listCmp.AppendItem(messages.NewMessageCmp(msg))
307}
308
309// handleToolMessage updates existing tool calls with their results.
310func (m *messageListCmp) handleToolMessage(msg message.Message) tea.Cmd {
311 items := m.listCmp.Items()
312 for _, tr := range msg.ToolResults() {
313 if toolCallIndex := m.findToolCallByID(items, tr.ToolCallID); toolCallIndex != NotFound {
314 toolCall := items[toolCallIndex].(messages.ToolCallCmp)
315 toolCall.SetToolResult(tr)
316 m.listCmp.UpdateItem(toolCall.ID(), toolCall)
317 }
318 }
319 return nil
320}
321
322// findToolCallByID searches for a tool call with the specified ID.
323// Returns the index if found, NotFound otherwise.
324func (m *messageListCmp) findToolCallByID(items []list.Item, toolCallID string) int {
325 // Search backwards as tool calls are more likely to be recent
326 for i := len(items) - 1; i >= 0; i-- {
327 if toolCall, ok := items[i].(messages.ToolCallCmp); ok && toolCall.GetToolCall().ID == toolCallID {
328 return i
329 }
330 }
331 return NotFound
332}
333
334// handleUpdateAssistantMessage processes updates to assistant messages,
335// managing both message content and associated tool calls.
336func (m *messageListCmp) handleUpdateAssistantMessage(msg message.Message) tea.Cmd {
337 var cmds []tea.Cmd
338 items := m.listCmp.Items()
339
340 // Find existing assistant message and tool calls for this message
341 assistantIndex, existingToolCalls := m.findAssistantMessageAndToolCalls(items, msg.ID)
342
343 // Handle assistant message content
344 if cmd := m.updateAssistantMessageContent(msg, assistantIndex); cmd != nil {
345 cmds = append(cmds, cmd)
346 }
347
348 // Handle tool calls
349 if cmd := m.updateToolCalls(msg, existingToolCalls); cmd != nil {
350 cmds = append(cmds, cmd)
351 }
352
353 return tea.Batch(cmds...)
354}
355
356// findAssistantMessageAndToolCalls locates the assistant message and its tool calls.
357func (m *messageListCmp) findAssistantMessageAndToolCalls(items []list.Item, messageID string) (int, map[int]messages.ToolCallCmp) {
358 assistantIndex := NotFound
359 toolCalls := make(map[int]messages.ToolCallCmp)
360
361 // Search backwards as messages are more likely to be at the end
362 for i := len(items) - 1; i >= 0; i-- {
363 item := items[i]
364 if asMsg, ok := item.(messages.MessageCmp); ok {
365 if asMsg.GetMessage().ID == messageID {
366 assistantIndex = i
367 }
368 } else if tc, ok := item.(messages.ToolCallCmp); ok {
369 if tc.ParentMessageID() == messageID {
370 toolCalls[i] = tc
371 }
372 }
373 }
374
375 return assistantIndex, toolCalls
376}
377
378// updateAssistantMessageContent updates or removes the assistant message based on content.
379func (m *messageListCmp) updateAssistantMessageContent(msg message.Message, assistantIndex int) tea.Cmd {
380 if assistantIndex == NotFound {
381 return nil
382 }
383
384 shouldShowMessage := m.shouldShowAssistantMessage(msg)
385 hasToolCallsOnly := len(msg.ToolCalls()) > 0 && msg.Content().Text == ""
386
387 var cmd tea.Cmd
388 if shouldShowMessage {
389 items := m.listCmp.Items()
390 uiMsg := items[assistantIndex].(messages.MessageCmp)
391 uiMsg.SetMessage(msg)
392 m.listCmp.UpdateItem(
393 items[assistantIndex].ID(),
394 uiMsg,
395 )
396 if msg.FinishPart() != nil && msg.FinishPart().Reason == message.FinishReasonEndTurn {
397 m.listCmp.AppendItem(
398 messages.NewAssistantSection(
399 msg,
400 time.Unix(m.lastUserMessageTime, 0),
401 ),
402 )
403 }
404 } else if hasToolCallsOnly {
405 items := m.listCmp.Items()
406 m.listCmp.DeleteItem(items[assistantIndex].ID())
407 }
408
409 return cmd
410}
411
412// shouldShowAssistantMessage determines if an assistant message should be displayed.
413func (m *messageListCmp) shouldShowAssistantMessage(msg message.Message) bool {
414 return len(msg.ToolCalls()) == 0 || msg.Content().Text != "" || msg.ReasoningContent().Thinking != "" || msg.IsThinking()
415}
416
417// updateToolCalls handles updates to tool calls, updating existing ones and adding new ones.
418func (m *messageListCmp) updateToolCalls(msg message.Message, existingToolCalls map[int]messages.ToolCallCmp) tea.Cmd {
419 var cmds []tea.Cmd
420
421 for _, tc := range msg.ToolCalls() {
422 if cmd := m.updateOrAddToolCall(msg, tc, existingToolCalls); cmd != nil {
423 cmds = append(cmds, cmd)
424 }
425 }
426
427 return tea.Batch(cmds...)
428}
429
430// updateOrAddToolCall updates an existing tool call or adds a new one.
431func (m *messageListCmp) updateOrAddToolCall(msg message.Message, tc message.ToolCall, existingToolCalls map[int]messages.ToolCallCmp) tea.Cmd {
432 // Try to find existing tool call
433 for _, existingTC := range existingToolCalls {
434 if tc.ID == existingTC.GetToolCall().ID {
435 existingTC.SetToolCall(tc)
436 if msg.FinishPart() != nil && msg.FinishPart().Reason == message.FinishReasonCanceled {
437 existingTC.SetCancelled()
438 }
439 m.listCmp.UpdateItem(tc.ID, existingTC)
440 return nil
441 }
442 }
443
444 // Add new tool call if not found
445 return m.listCmp.AppendItem(messages.NewToolCallCmp(msg.ID, tc, m.app.Permissions))
446}
447
448// handleNewAssistantMessage processes new assistant messages and their tool calls.
449func (m *messageListCmp) handleNewAssistantMessage(msg message.Message) tea.Cmd {
450 var cmds []tea.Cmd
451
452 // Add assistant message if it should be displayed
453 if m.shouldShowAssistantMessage(msg) {
454 cmd := m.listCmp.AppendItem(
455 messages.NewMessageCmp(
456 msg,
457 ),
458 )
459 cmds = append(cmds, cmd)
460 }
461
462 // Add tool calls
463 for _, tc := range msg.ToolCalls() {
464 cmd := m.listCmp.AppendItem(messages.NewToolCallCmp(msg.ID, tc, m.app.Permissions))
465 cmds = append(cmds, cmd)
466 }
467
468 return tea.Batch(cmds...)
469}
470
471// SetSession loads and displays messages for a new session.
472func (m *messageListCmp) SetSession(session session.Session) tea.Cmd {
473 if m.session.ID == session.ID {
474 return nil
475 }
476
477 m.session = session
478 sessionMessages, err := m.app.Messages.List(context.Background(), session.ID)
479 if err != nil {
480 return util.ReportError(err)
481 }
482
483 if len(sessionMessages) == 0 {
484 return m.listCmp.SetItems([]list.Item{})
485 }
486
487 // Initialize with first message timestamp
488 m.lastUserMessageTime = sessionMessages[0].CreatedAt
489
490 // Build tool result map for efficient lookup
491 toolResultMap := m.buildToolResultMap(sessionMessages)
492
493 // Convert messages to UI components
494 uiMessages := m.convertMessagesToUI(sessionMessages, toolResultMap)
495
496 return m.listCmp.SetItems(uiMessages)
497}
498
499// buildToolResultMap creates a map of tool call ID to tool result for efficient lookup.
500func (m *messageListCmp) buildToolResultMap(messages []message.Message) map[string]message.ToolResult {
501 toolResultMap := make(map[string]message.ToolResult)
502 for _, msg := range messages {
503 for _, tr := range msg.ToolResults() {
504 toolResultMap[tr.ToolCallID] = tr
505 }
506 }
507 return toolResultMap
508}
509
510// convertMessagesToUI converts database messages to UI components.
511func (m *messageListCmp) convertMessagesToUI(sessionMessages []message.Message, toolResultMap map[string]message.ToolResult) []list.Item {
512 uiMessages := make([]list.Item, 0)
513
514 for _, msg := range sessionMessages {
515 switch msg.Role {
516 case message.User:
517 m.lastUserMessageTime = msg.CreatedAt
518 uiMessages = append(uiMessages, messages.NewMessageCmp(msg))
519 case message.Assistant:
520 uiMessages = append(uiMessages, m.convertAssistantMessage(msg, toolResultMap)...)
521 if msg.FinishPart() != nil && msg.FinishPart().Reason == message.FinishReasonEndTurn {
522 uiMessages = append(uiMessages, messages.NewAssistantSection(msg, time.Unix(m.lastUserMessageTime, 0)))
523 }
524 }
525 }
526
527 return uiMessages
528}
529
530// convertAssistantMessage converts an assistant message and its tool calls to UI components.
531func (m *messageListCmp) convertAssistantMessage(msg message.Message, toolResultMap map[string]message.ToolResult) []list.Item {
532 var uiMessages []list.Item
533
534 // Add assistant message if it should be displayed
535 if m.shouldShowAssistantMessage(msg) {
536 uiMessages = append(
537 uiMessages,
538 messages.NewMessageCmp(
539 msg,
540 ),
541 )
542 }
543
544 // Add tool calls with their results and status
545 for _, tc := range msg.ToolCalls() {
546 options := m.buildToolCallOptions(tc, msg, toolResultMap)
547 uiMessages = append(uiMessages, messages.NewToolCallCmp(msg.ID, tc, m.app.Permissions, options...))
548 // If this tool call is the agent tool, fetch nested tool calls
549 if tc.Name == agent.AgentToolName {
550 nestedMessages, _ := m.app.Messages.List(context.Background(), tc.ID)
551 nestedToolResultMap := m.buildToolResultMap(nestedMessages)
552 nestedUIMessages := m.convertMessagesToUI(nestedMessages, nestedToolResultMap)
553 nestedToolCalls := make([]messages.ToolCallCmp, 0, len(nestedUIMessages))
554 for _, nestedMsg := range nestedUIMessages {
555 if toolCall, ok := nestedMsg.(messages.ToolCallCmp); ok {
556 toolCall.SetIsNested(true)
557 nestedToolCalls = append(nestedToolCalls, toolCall)
558 }
559 }
560 uiMessages[len(uiMessages)-1].(messages.ToolCallCmp).SetNestedToolCalls(nestedToolCalls)
561 }
562 }
563
564 return uiMessages
565}
566
567// buildToolCallOptions creates options for tool call components based on results and status.
568func (m *messageListCmp) buildToolCallOptions(tc message.ToolCall, msg message.Message, toolResultMap map[string]message.ToolResult) []messages.ToolCallOption {
569 var options []messages.ToolCallOption
570
571 // Add tool result if available
572 if tr, ok := toolResultMap[tc.ID]; ok {
573 options = append(options, messages.WithToolCallResult(tr))
574 }
575
576 // Add cancelled status if applicable
577 if msg.FinishPart() != nil && msg.FinishPart().Reason == message.FinishReasonCanceled {
578 options = append(options, messages.WithToolCallCancelled())
579 }
580
581 return options
582}
583
584// GetSize returns the current width and height of the component.
585func (m *messageListCmp) GetSize() (int, int) {
586 return m.width, m.height
587}
588
589// SetSize updates the component dimensions and propagates to the list component.
590func (m *messageListCmp) SetSize(width int, height int) tea.Cmd {
591 m.width = width
592 m.height = height
593 return m.listCmp.SetSize(width-2, height-1) // for padding
594}
595
596// Blur implements MessageListCmp.
597func (m *messageListCmp) Blur() tea.Cmd {
598 return m.listCmp.Blur()
599}
600
601// Focus implements MessageListCmp.
602func (m *messageListCmp) Focus() tea.Cmd {
603 return m.listCmp.Focus()
604}
605
606// IsFocused implements MessageListCmp.
607func (m *messageListCmp) IsFocused() bool {
608 return m.listCmp.IsFocused()
609}
610
611func (m *messageListCmp) Bindings() []key.Binding {
612 return m.defaultListKeyMap.KeyBindings()
613}
614
615func (m *messageListCmp) GoToBottom() tea.Cmd {
616 return m.listCmp.GoToBottom()
617}
618
619// handleMouseClick handles mouse click events and detects double/triple clicks.
620func (m *messageListCmp) handleMouseClick(x, y int) tea.Cmd {
621 const (
622 doubleClickThreshold = 500 * time.Millisecond
623 clickTolerance = 2 // pixels
624 )
625
626 now := time.Now()
627
628 // Check if this is a potential multi-click
629 if now.Sub(m.lastClickTime) <= doubleClickThreshold &&
630 abs(x-m.lastClickX) <= clickTolerance &&
631 abs(y-m.lastClickY) <= clickTolerance {
632 m.clickCount++
633 } else {
634 m.clickCount = 1
635 }
636
637 m.lastClickTime = now
638 m.lastClickX = x
639 m.lastClickY = y
640
641 switch m.clickCount {
642 case 1:
643 // Single click - start selection
644 m.listCmp.StartSelection(x, y)
645 case 2:
646 // Double click - select word
647 m.listCmp.SelectWord(x, y)
648 case 3:
649 // Triple click - select paragraph
650 m.listCmp.SelectParagraph(x, y)
651 m.clickCount = 0 // Reset after triple click
652 }
653
654 return nil
655}
656
657// SelectionClear clears the current selection in the list component.
658func (m *messageListCmp) SelectionClear() tea.Cmd {
659 m.listCmp.SelectionClear()
660 m.previousSelected = ""
661 m.lastClickX, m.lastClickY = 0, 0
662 m.clickCount = 0
663 return nil
664}
665
666// HasSelection checks if there is a selection in the list component.
667func (m *messageListCmp) HasSelection() bool {
668 return m.listCmp.HasSelection()
669}
670
671// GetSelectedText returns the currently selected text from the list component.
672func (m *messageListCmp) GetSelectedText() string {
673 return m.listCmp.GetSelectedText(3) // 3 padding for the left border/padding
674}
675
676// CopySelectedText copies the currently selected text to the clipboard. When
677// clear is true, it clears the selection after copying.
678func (m *messageListCmp) CopySelectedText(clear bool) tea.Cmd {
679 if !m.listCmp.HasSelection() {
680 return nil
681 }
682
683 selectedText := m.GetSelectedText()
684 if selectedText == "" {
685 return util.ReportInfo("No text selected")
686 }
687
688 if clear {
689 defer func() { m.SelectionClear() }()
690 }
691
692 return tea.Sequence(
693 // We use both OSC 52 and native clipboard for compatibility with different
694 // terminal emulators and environments.
695 tea.SetClipboard(selectedText),
696 func() tea.Msg {
697 _ = clipboard.WriteAll(selectedText)
698 return nil
699 },
700 util.ReportInfo("Selected text copied to clipboard"),
701 )
702}
703
704// abs returns the absolute value of an integer.
705func abs(x int) int {
706 if x < 0 {
707 return -x
708 }
709 return x
710}