wip

Ayman Bagabas created

Change summary

internal/tui/components/chat/chat.go | 67 ++++++++++++++++++++++++++---
internal/tui/exp/list/list.go        | 53 +++++++++++++++++++++++
internal/tui/page/chat/chat.go       |  2 
3 files changed, 113 insertions(+), 9 deletions(-)

Detailed changes

internal/tui/components/chat/chat.go 🔗

@@ -17,6 +17,7 @@ import (
 	"github.com/charmbracelet/crush/internal/tui/exp/list"
 	"github.com/charmbracelet/crush/internal/tui/styles"
 	"github.com/charmbracelet/crush/internal/tui/util"
+	uv "github.com/charmbracelet/ultraviolet"
 )
 
 type SendMsg struct {
@@ -56,6 +57,8 @@ type messageListCmp struct {
 
 	lastUserMessageTime int64
 	defaultListKeyMap   list.KeyMap
+
+	selStart, selEnd uv.Position
 }
 
 // New creates a new message list component with custom keybindings
@@ -85,6 +88,7 @@ func (m *messageListCmp) Init() tea.Cmd {
 
 // Update handles incoming messages and updates the component state.
 func (m *messageListCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+	var cmds []tea.Cmd
 	switch msg := msg.(type) {
 	case pubsub.Event[permission.PermissionNotification]:
 		return m, m.handlePermissionRequest(msg.Payload)
@@ -102,29 +106,78 @@ func (m *messageListCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		cmd := m.handleMessageEvent(msg)
 		return m, cmd
 
+	case tea.MouseClickMsg:
+		if msg.Button == tea.MouseLeft {
+			m.listCmp.StartSelection(msg.X, msg.Y)
+		}
+
+	case tea.MouseMotionMsg:
+		if msg.Button == tea.MouseLeft {
+			m.listCmp.EndSelection(msg.X, msg.Y)
+			if msg.Y <= 1 {
+				// Scroll up while dragging the mouse
+				cmds = append(cmds, m.listCmp.MoveUp(1))
+			} else if msg.Y >= m.height-1 {
+				// Scroll down while dragging the mouse
+				cmds = append(cmds, m.listCmp.MoveDown(1))
+			}
+		}
+
+	case tea.MouseReleaseMsg:
+		if msg.Button == tea.MouseLeft {
+			m.listCmp.EndSelection(msg.X, msg.Y)
+		}
+
 	case tea.MouseWheelMsg:
 		u, cmd := m.listCmp.Update(msg)
 		m.listCmp = u.(list.List[list.Item])
 		return m, cmd
-	default:
-		var cmds []tea.Cmd
-		u, cmd := m.listCmp.Update(msg)
-		m.listCmp = u.(list.List[list.Item])
-		cmds = append(cmds, cmd)
-		return m, tea.Batch(cmds...)
 	}
+
+	u, cmd := m.listCmp.Update(msg)
+	m.listCmp = u.(list.List[list.Item])
+	cmds = append(cmds, cmd)
+
+	return m, tea.Batch(cmds...)
 }
 
+var zeroPos = uv.Position{}
+
 // View renders the message list or an initial screen if empty.
 func (m *messageListCmp) View() string {
 	t := styles.CurrentTheme()
-	return t.S().Base.
+	view := t.S().Base.
 		Padding(1, 1, 0, 1).
 		Width(m.width).
 		Height(m.height).
 		Render(
 			m.listCmp.View(),
 		)
+
+	area := uv.Rect(0, 0, m.width, m.height)
+	scr := uv.NewScreenBuffer(area.Dx(), area.Dy())
+	uv.NewStyledString(view).Draw(scr, area)
+	if m.selStart != zeroPos && m.selEnd != zeroPos {
+		selArea := uv.Rectangle{
+			Min: m.selStart,
+			Max: m.selEnd,
+		}
+		if m.selStart.X > m.selEnd.X || (m.selStart.X == m.selEnd.X && m.selStart.Y > m.selEnd.Y) {
+			selArea.Min, selArea.Max = selArea.Max, selArea.Min
+		}
+		for y := 0; y < area.Dy(); y++ {
+			for x := 0; x < area.Dx(); x++ {
+				cell := scr.CellAt(x, y)
+				if cell != nil && uv.Pos(x, y).In(selArea) {
+					cell = cell.Clone()
+					cell.Style = cell.Style.Reverse(true)
+					scr.SetCell(x, y, cell)
+				}
+			}
+		}
+	}
+
+	return scr.Render()
 }
 
 func (m *messageListCmp) handlePermissionRequest(permission permission.PermissionNotification) tea.Cmd {

internal/tui/exp/list/list.go 🔗

@@ -12,6 +12,7 @@ import (
 	"github.com/charmbracelet/crush/internal/tui/styles"
 	"github.com/charmbracelet/crush/internal/tui/util"
 	"github.com/charmbracelet/lipgloss/v2"
+	uv "github.com/charmbracelet/ultraviolet"
 )
 
 type Item interface {
@@ -45,6 +46,8 @@ type List[T Item] interface {
 	DeleteItem(string) tea.Cmd
 	PrependItem(T) tea.Cmd
 	AppendItem(T) tea.Cmd
+	StartSelection(col, line int)
+	EndSelection(col, line int)
 }
 
 type direction int
@@ -93,6 +96,11 @@ type list[T Item] struct {
 	rendered string
 
 	movingByItem bool
+
+	selectionStartCol  int
+	selectionStartLine int
+	selectionEndCol    int
+	selectionEndLine   int
 }
 
 type ListOption func(*confOptions)
@@ -187,6 +195,20 @@ func New[T Item](items []T, opts ...ListOption) List[T] {
 	return list
 }
 
+// StartSelection implements List.
+func (l *list[T]) StartSelection(col, line int) {
+	l.selectionStartCol = col
+	l.selectionStartLine = line
+	l.selectionEndCol = col
+	l.selectionEndLine = line
+}
+
+// EndSelection implements List.
+func (l *list[T]) EndSelection(col, line int) {
+	l.selectionEndCol = col
+	l.selectionEndLine = line
+}
+
 // Init implements List.
 func (l *list[T]) Init() tea.Cmd {
 	return l.render()
@@ -280,10 +302,39 @@ func (l *list[T]) View() string {
 	if l.resize {
 		return strings.Join(lines, "\n")
 	}
-	return t.S().Base.
+
+	view = t.S().Base.
 		Height(l.height).
 		Width(l.width).
 		Render(strings.Join(lines, "\n"))
+
+	area := uv.Rect(0, 0, l.width, l.height)
+	scr := uv.NewScreenBuffer(area.Dx(), area.Dy())
+	uv.NewStyledString(view).Draw(scr, area)
+	selArea := uv.Rectangle{
+		Min: uv.Pos(l.selectionStartCol, l.selectionStartLine),
+		Max: uv.Pos(l.selectionEndCol, l.selectionEndLine),
+	}
+	selArea = selArea.Canon()
+	for y := 0; y < scr.Height(); y++ {
+		for x := 0; x < scr.Width(); x++ {
+			cell := scr.CellAt(x, y)
+			firstLine := y == selArea.Min.Y && x >= l.selectionStartCol && x < scr.Width()
+			lastLine := y == selArea.Max.Y-1 && x >= 0 && x < l.selectionEndCol
+			middleLine := y > selArea.Min.Y && y < selArea.Max.Y-1 && x >= 0 && x < scr.Width()
+			if cell != nil &&
+				(y >= selArea.Min.Y && y < selArea.Max.Y) &&
+				(firstLine || // First line until the end
+					lastLine || // Last line until the end
+					middleLine) { // Middle lines
+				cell = cell.Clone()
+				cell.Style = cell.Style.Reverse(true)
+				scr.SetCell(x, y, cell)
+			}
+		}
+	}
+
+	return scr.Render()
 }
 
 func (l *list[T]) viewPosition() (int, int) {

internal/tui/page/chat/chat.go 🔗

@@ -164,7 +164,7 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	case tea.KeyboardEnhancementsMsg:
 		p.keyboardEnhancements = msg
 		return p, nil
-	case tea.MouseWheelMsg:
+	case tea.MouseMsg:
 		if p.isMouseOverChat(msg.Mouse().X, msg.Mouse().Y) {
 			u, cmd := p.chat.Update(msg)
 			p.chat = u.(chat.MessageListCmp)