@@ -2,7 +2,6 @@ package chat
8 "github.com/atotto/clipboard"
7 "github.com/atotto/clipboard"
@@ -45,7 +44,7 @@ type MessageListCmp interface {
45 SetSession(session.Session) tea.Cmd
44 SetSession(session.Session) tea.Cmd
46 GoToBottom() tea.Cmd
45 GoToBottom() tea.Cmd
47 GetSelectedText() string
46 GetSelectedText() string
48− CopySelectedText() tea.Cmd
47+ CopySelectedText(bool) tea.Cmd
51// messageListCmp implements MessageListCmp, providing a virtualized list
50// messageListCmp implements MessageListCmp, providing a virtualized list
@@ -96,6 +95,10 @@ func (m *messageListCmp) Init() tea.Cmd {
96// Update handles incoming messages and updates the component state.
95// Update handles incoming messages and updates the component state.
97func (m *messageListCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
96func (m *messageListCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
98 switch msg := msg.(type) {
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)
99 case tea.MouseClickMsg:
102 case tea.MouseClickMsg:
100 x := msg.X - 1 // Adjust for padding
103 x := msg.X - 1 // Adjust for padding
101 y := msg.Y - 1 // Adjust for padding
104 y := msg.Y - 1 // Adjust for padding
@@ -128,11 +131,10 @@ func (m *messageListCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
128 if msg.Button == tea.MouseLeft {
131 if msg.Button == tea.MouseLeft {
129 if x < 0 || y < 0 || x >= m.width-2 || y >= m.height-1 {
132 if x < 0 || y < 0 || x >= m.width-2 || y >= m.height-1 {
130 m.listCmp.SelectionStop()
133 m.listCmp.SelectionStop()
131− return m, m.CopySelectedText()
134+ } else {
135+ m.listCmp.EndSelection(x, y)
136+ m.listCmp.SelectionStop()
133− m.listCmp.EndSelection(x, y)
134− m.listCmp.SelectionStop()
135− return m, m.CopySelectedText()
137 return m, nil
139 return m, nil
138 case pubsub.Event[permission.PermissionNotification]:
140 case pubsub.Event[permission.PermissionNotification]:
@@ -155,13 +157,11 @@ func (m *messageListCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
155 u, cmd := m.listCmp.Update(msg)
157 u, cmd := m.listCmp.Update(msg)
156 m.listCmp = u.(list.List[list.Item])
158 m.listCmp = u.(list.List[list.Item])
157 return m, cmd
159 return m, cmd
160− u, cmd := m.listCmp.Update(msg)
161− m.listCmp = u.(list.List[list.Item])
162− cmds = append(cmds, cmd)
163− return m, tea.Batch(cmds...)
162+ u, cmd := m.listCmp.Update(msg)
163+ m.listCmp = u.(list.List[list.Item])
167// View renders the message list or an initial screen if empty.
167// View renders the message list or an initial screen if empty.
@@ -654,24 +654,51 @@ func (m *messageListCmp) handleMouseClick(x, y int) tea.Cmd {
654 return nil
654 return nil
657+// SelectionClear clears the current selection in the list component.
658+func (m *messageListCmp) SelectionClear() tea.Cmd {
659+ m.listCmp.SelectionClear()
660+ m.previousSelected = ""
661+ m.lastClickX, m.lastClickY = 0, 0
666+// HasSelection checks if there is a selection in the list component.
667+func (m *messageListCmp) HasSelection() bool {
668+ return m.listCmp.HasSelection()
657// GetSelectedText returns the currently selected text from the list component.
671// GetSelectedText returns the currently selected text from the list component.
658func (m *messageListCmp) GetSelectedText() string {
672func (m *messageListCmp) GetSelectedText() string {
659 return m.listCmp.GetSelectedText(3) // 3 padding for the left border/padding
673 return m.listCmp.GetSelectedText(3) // 3 padding for the left border/padding
662−// CopySelectedText copies the currently selected text to the clipboard.
676+// CopySelectedText copies the currently selected text to the clipboard. When
677+// clear is true, it clears the selection after copying.
663−func (m *messageListCmp) CopySelectedText() tea.Cmd {
678+func (m *messageListCmp) CopySelectedText(clear bool) tea.Cmd {
679+ if !m.listCmp.HasSelection() {
664− return nil
680+ return nil
665 selectedText := m.GetSelectedText()
683 selectedText := m.GetSelectedText()
666 if selectedText == "" {
684 if selectedText == "" {
667 return util.ReportInfo("No text selected")
685 return util.ReportInfo("No text selected")
670− err := clipboard.WriteAll(selectedText)
688+ if clear {
671− if err != nil {
689+ defer func() { m.SelectionClear() }()
672− return util.ReportError(fmt.Errorf("failed to copy selected text to clipboard: %w", err))
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),
697+ _ = clipboard.WriteAll(selectedText)
674− return util.ReportInfo("Selected text copied to clipboard")
700+ util.ReportInfo("Selected text copied to clipboard"),
677// abs returns the absolute value of an integer.
704// abs returns the absolute value of an integer.