Detailed changes
@@ -10,16 +10,16 @@ require (
github.com/anthropics/anthropic-sdk-go v1.6.2
github.com/atotto/clipboard v0.1.4
github.com/aymanbagabas/go-udiff v0.3.1
- github.com/bmatcuk/doublestar/v4 v4.9.0
+ github.com/bmatcuk/doublestar/v4 v4.9.1
github.com/charlievieth/fastwalk v1.0.11
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250716191546-1e2ffbbcf5c5
- github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4.0.20250730165737-56ff7146d52d
- github.com/charmbracelet/catwalk v0.4.5
+ github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4.0.20250805190305-70e94a2e0b2d
+ github.com/charmbracelet/catwalk v0.4.6
github.com/charmbracelet/fang v0.3.1-0.20250711140230-d5ebb8c1d674
github.com/charmbracelet/glamour/v2 v2.0.0-20250516160903-6f1e2c8f9ebe
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250721205738-ea66aa652ee0
github.com/charmbracelet/log/v2 v2.0.0-20250226163916-c379e29ff706
- github.com/charmbracelet/x/ansi v0.9.3
+ github.com/charmbracelet/x/ansi v0.10.0
github.com/charmbracelet/x/exp/charmtone v0.0.0-20250708181618-a60a724ba6c3
github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a
github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec
@@ -75,7 +75,7 @@ require (
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/charmbracelet/colorprofile v0.3.1 // indirect
- github.com/charmbracelet/ultraviolet v0.0.0-20250731212901-76da584cc9a5
+ github.com/charmbracelet/ultraviolet v0.0.0-20250805154935-01be9d7ef65d
github.com/charmbracelet/x/cellbuf v0.0.14-0.20250516160309-24eee56f89fa // indirect
github.com/charmbracelet/x/exp/slice v0.0.0-20250611152503-f53cdd7e01ef
github.com/charmbracelet/x/term v0.2.1
@@ -68,18 +68,18 @@ github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuP
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
-github.com/bmatcuk/doublestar/v4 v4.9.0 h1:DBvuZxjdKkRP/dr4GVV4w2fnmrk5Hxc90T51LZjv0JA=
-github.com/bmatcuk/doublestar/v4 v4.9.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
+github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE=
+github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/charlievieth/fastwalk v1.0.11 h1:5sLT/q9+d9xMdpKExawLppqvXFZCVKf6JHnr2u/ufj8=
github.com/charlievieth/fastwalk v1.0.11/go.mod h1:yGy1zbxog41ZVMcKA/i8ojXLFsuayX5VvwhQVoj9PBI=
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250716191546-1e2ffbbcf5c5 h1:GTcMIfDQJKyNKS+xVt7GkNIwz+tBuQtIuiP50WpzNgs=
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250716191546-1e2ffbbcf5c5/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw=
-github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4.0.20250730165737-56ff7146d52d h1:YMXLZHSo8DjytVY/b5dK8LDuyQsVUmBK3ydQMpu2Ui4=
-github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4.0.20250730165737-56ff7146d52d/go.mod h1:XIQ1qQfRph6Z5o2EikCydjumo0oDInQySRHuPATzbZc=
-github.com/charmbracelet/catwalk v0.4.5 h1:Kv3PadDe8IF8gpcYTfAJdCee5Bv4HufvtNT61FXtq5g=
-github.com/charmbracelet/catwalk v0.4.5/go.mod h1:WnKgNPmQHuMyk7GtwAQwl+ezHusfH40IvzML2qwUGwc=
+github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4.0.20250805190305-70e94a2e0b2d h1:1C2whi5rgs+APtsQDE4riA+W5neYaMNY+Y+5o6J/rDU=
+github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4.0.20250805190305-70e94a2e0b2d/go.mod h1:XIQ1qQfRph6Z5o2EikCydjumo0oDInQySRHuPATzbZc=
+github.com/charmbracelet/catwalk v0.4.6 h1:Y0JDq5V4agK8oO3lKC/hhInrYXePGwZPNo8I1Lk08jc=
+github.com/charmbracelet/catwalk v0.4.6/go.mod h1:WnKgNPmQHuMyk7GtwAQwl+ezHusfH40IvzML2qwUGwc=
github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40=
github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0=
github.com/charmbracelet/fang v0.3.1-0.20250711140230-d5ebb8c1d674 h1:+Cz+VfxD5DO+JT1LlswXWhre0HYLj6l2HW8HVGfMuC0=
@@ -90,10 +90,10 @@ github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250721205738-ea66aa652ee0
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250721205738-ea66aa652ee0/go.mod h1:XIuqKpZTUXtVyeyiN1k9Tc/U7EzfaDnVc34feFHfBws=
github.com/charmbracelet/log/v2 v2.0.0-20250226163916-c379e29ff706 h1:WkwO6Ks3mSIGnGuSdKl9qDSyfbYK50z2wc2gGMggegE=
github.com/charmbracelet/log/v2 v2.0.0-20250226163916-c379e29ff706/go.mod h1:mjJGp00cxcfvD5xdCa+bso251Jt4owrQvuimJtVmEmM=
-github.com/charmbracelet/ultraviolet v0.0.0-20250731212901-76da584cc9a5 h1:FrEzjuUbVbGd8UtZBfK8mf/IA4ExT2i3/fi+SEOv2eM=
-github.com/charmbracelet/ultraviolet v0.0.0-20250731212901-76da584cc9a5/go.mod h1:XrrgNFfXLrFAyd9DUmrqVc3yQFVv8Uk+okj4PsNNzpc=
-github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0=
-github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
+github.com/charmbracelet/ultraviolet v0.0.0-20250805154935-01be9d7ef65d h1:miSXsyi0ARm35O+DulTdaLYoUioWdGkoBfFoIhdqpCA=
+github.com/charmbracelet/ultraviolet v0.0.0-20250805154935-01be9d7ef65d/go.mod h1:XrrgNFfXLrFAyd9DUmrqVc3yQFVv8Uk+okj4PsNNzpc=
+github.com/charmbracelet/x/ansi v0.10.0 h1:jnOP9pFxY6/gw5nYjkpi6f17K0P/sN4fqT0Y1ioaORI=
+github.com/charmbracelet/x/ansi v0.10.0/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
github.com/charmbracelet/x/cellbuf v0.0.14-0.20250516160309-24eee56f89fa h1:lphz0Z3rsiOtMYiz8axkT24i9yFiueDhJbzyNUADmME=
github.com/charmbracelet/x/cellbuf v0.0.14-0.20250516160309-24eee56f89fa/go.mod h1:xBlh2Yi3DL3zy/2n15kITpg0YZardf/aa/hgUaIM6Rk=
github.com/charmbracelet/x/exp/charmtone v0.0.0-20250708181618-a60a724ba6c3 h1:1xwHZg6eMZ9Wv5TE1UGub6ARubyOd1Lo5kPUI/6VL50=
@@ -2,7 +2,6 @@ package chat
import (
"context"
- "fmt"
"time"
"github.com/atotto/clipboard"
@@ -45,7 +44,7 @@ type MessageListCmp interface {
SetSession(session.Session) tea.Cmd
GoToBottom() tea.Cmd
GetSelectedText() string
- CopySelectedText() tea.Cmd
+ CopySelectedText(bool) tea.Cmd
}
// messageListCmp implements MessageListCmp, providing a virtualized list
@@ -96,6 +95,10 @@ 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) {
switch msg := msg.(type) {
+ case tea.KeyPressMsg:
+ if key.Matches(msg, messages.CopyKey) && m.listCmp.HasSelection() {
+ return m, m.CopySelectedText(true)
+ }
case tea.MouseClickMsg:
x := msg.X - 1 // Adjust for padding
y := msg.Y - 1 // Adjust for padding
@@ -128,11 +131,10 @@ func (m *messageListCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if msg.Button == tea.MouseLeft {
if x < 0 || y < 0 || x >= m.width-2 || y >= m.height-1 {
m.listCmp.SelectionStop()
- return m, m.CopySelectedText()
+ } else {
+ m.listCmp.EndSelection(x, y)
+ m.listCmp.SelectionStop()
}
- m.listCmp.EndSelection(x, y)
- m.listCmp.SelectionStop()
- return m, m.CopySelectedText()
}
return m, nil
case pubsub.Event[permission.PermissionNotification]:
@@ -155,13 +157,11 @@ func (m *messageListCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
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])
+ return m, cmd
}
// View renders the message list or an initial screen if empty.
@@ -654,24 +654,51 @@ func (m *messageListCmp) handleMouseClick(x, y int) tea.Cmd {
return nil
}
+// SelectionClear clears the current selection in the list component.
+func (m *messageListCmp) SelectionClear() tea.Cmd {
+ m.listCmp.SelectionClear()
+ m.previousSelected = ""
+ m.lastClickX, m.lastClickY = 0, 0
+ m.clickCount = 0
+ return nil
+}
+
+// HasSelection checks if there is a selection in the list component.
+func (m *messageListCmp) HasSelection() bool {
+ return m.listCmp.HasSelection()
+}
+
// GetSelectedText returns the currently selected text from the list component.
func (m *messageListCmp) GetSelectedText() string {
return m.listCmp.GetSelectedText(3) // 3 padding for the left border/padding
}
-// CopySelectedText copies the currently selected text to the clipboard.
-func (m *messageListCmp) CopySelectedText() tea.Cmd {
- return nil
+// CopySelectedText copies the currently selected text to the clipboard. When
+// clear is true, it clears the selection after copying.
+func (m *messageListCmp) CopySelectedText(clear bool) tea.Cmd {
+ if !m.listCmp.HasSelection() {
+ return nil
+ }
+
selectedText := m.GetSelectedText()
if selectedText == "" {
return util.ReportInfo("No text selected")
}
- err := clipboard.WriteAll(selectedText)
- if err != nil {
- return util.ReportError(fmt.Errorf("failed to copy selected text to clipboard: %w", err))
+ if clear {
+ defer func() { m.SelectionClear() }()
}
- return util.ReportInfo("Selected text copied to clipboard")
+
+ return tea.Sequence(
+ // We use both OSC 52 and native clipboard for compatibility with different
+ // terminal emulators and environments.
+ tea.SetClipboard(selectedText),
+ func() tea.Msg {
+ _ = clipboard.WriteAll(selectedText)
+ return nil
+ },
+ util.ReportInfo("Selected text copied to clipboard"),
+ )
}
// abs returns the absolute value of an integer.
@@ -25,7 +25,8 @@ import (
"github.com/charmbracelet/crush/internal/tui/util"
)
-var copyKey = key.NewBinding(key.WithKeys("c", "y", "C", "Y"), key.WithHelp("c/y", "copy"))
+// CopyKey is the key binding for copying message content to the clipboard.
+var CopyKey = key.NewBinding(key.WithKeys("c", "y", "C", "Y"), key.WithHelp("c/y", "copy"))
// MessageCmp defines the interface for message components in the chat interface.
// It combines standard UI model interfaces with message-specific functionality.
@@ -99,12 +100,15 @@ func (m *messageCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, cmd
}
case tea.KeyPressMsg:
- if key.Matches(msg, copyKey) {
- err := clipboard.WriteAll(m.message.Content().Text)
- if err != nil {
- return m, util.ReportError(fmt.Errorf("failed to copy message content to clipboard: %w", err))
- }
- return m, util.ReportInfo("Message copied to clipboard")
+ if key.Matches(msg, CopyKey) {
+ return m, tea.Sequence(
+ tea.SetClipboard(m.message.Content().Text),
+ func() tea.Msg {
+ _ = clipboard.WriteAll(m.message.Content().Text)
+ return nil
+ },
+ util.ReportInfo("Message copied to clipboard"),
+ )
}
}
return m, nil
@@ -165,7 +165,7 @@ func (m *toolCallCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
return m, tea.Batch(cmds...)
case tea.KeyPressMsg:
- if key.Matches(msg, copyKey) {
+ if key.Matches(msg, CopyKey) {
return m, m.copyTool()
}
}
@@ -198,11 +198,14 @@ func (m *toolCallCmp) SetCancelled() {
func (m *toolCallCmp) copyTool() tea.Cmd {
content := m.formatToolForCopy()
- err := clipboard.WriteAll(content)
- if err != nil {
- return util.ReportError(fmt.Errorf("failed to copy tool content to clipboard: %w", err))
- }
- return util.ReportInfo("Tool content copied to clipboard")
+ return tea.Sequence(
+ tea.SetClipboard(content),
+ func() tea.Msg {
+ _ = clipboard.WriteAll(content)
+ return nil
+ },
+ util.ReportInfo("Tool content copied to clipboard"),
+ )
}
func (m *toolCallCmp) formatToolForCopy() string {
@@ -42,7 +42,7 @@ func DefaultKeyMap() KeyMap {
key.WithHelp("s", "allow session"),
),
Deny: key.NewBinding(
- key.WithKeys("d", "D", "ctrl+d"),
+ key.WithKeys("d", "D", "ctrl+d", "esc"),
key.WithHelp("d", "deny"),
),
Select: key.NewBinding(
@@ -68,7 +68,7 @@ func RenderMCPList(opts RenderOptions) []string {
case agent.MCPStateConnected:
icon = t.ItemOnlineIcon
if state.ToolCount > 0 {
- extraContent = t.S().Subtle.Render(fmt.Sprintf("(%d tools)", state.ToolCount))
+ extraContent = t.S().Subtle.Render(fmt.Sprintf("%d tools", state.ToolCount))
}
case agent.MCPStateError:
icon = t.ItemErrorIcon
@@ -56,6 +56,7 @@ type List[T Item] interface {
SelectWord(col, line int)
SelectParagraph(col, line int)
GetSelectedText(paddingLeft int) string
+ HasSelection() bool
}
type direction int
@@ -286,30 +287,10 @@ func (l *list[T]) handleMouseWheel(msg tea.MouseWheelMsg) (tea.Model, tea.Cmd) {
return l, cmd
}
-// View implements List.
-func (l *list[T]) View() string {
- if l.height <= 0 || l.width <= 0 {
- return ""
- }
+// selectionView renders the highlighted selection in the view and returns it
+// as a string. If textOnly is true, it won't render any styles.
+func (l *list[T]) selectionView(view string, textOnly bool) string {
t := styles.CurrentTheme()
- view := l.rendered
- lines := strings.Split(view, "\n")
-
- start, end := l.viewPosition()
- viewStart := max(0, start)
- viewEnd := min(len(lines), end+1)
- lines = lines[viewStart:viewEnd]
- if l.resize {
- return strings.Join(lines, "\n")
- }
- view = t.S().Base.
- Height(l.height).
- Width(l.width).
- Render(strings.Join(lines, "\n"))
-
- if !l.hasSelection() {
- return view
- }
area := uv.Rect(0, 0, l.width, l.height)
scr := uv.NewScreenBuffer(area.Dx(), area.Dy())
uv.NewStyledString(view).Draw(scr, area)
@@ -397,6 +378,8 @@ func (l *list[T]) View() string {
lineTextBounds[y] = bounds
}
+ var selectedText strings.Builder
+
// Second pass: apply selection highlighting
for y := range scr.Height() {
selBounds := lineSelections[y]
@@ -406,6 +389,11 @@ func (l *list[T]) View() string {
textBounds := lineTextBounds[y]
if textBounds.start < 0 {
+ if textOnly {
+ // We don't want to get rid of all empty lines in text-only mode
+ selectedText.WriteByte('\n')
+ }
+
continue // No text on this line
}
@@ -421,16 +409,63 @@ func (l *list[T]) View() string {
cellStr := cell.String()
if len(cellStr) > 0 && !specialChars[cellStr] {
+ if textOnly {
+ // Collect selected text without styles
+ selectedText.WriteString(cell.String())
+ continue
+ }
+
+ // Text selection styling, which is a Lip Gloss style. We must
+ // extract the values to use in a UV style, below.
+ ts := t.TextSelection
+
cell = cell.Clone()
- cell.Style = cell.Style.Background(t.BgOverlay).Foreground(t.White)
+ cell.Style = cell.Style.Background(ts.GetBackground()).Foreground(ts.GetForeground())
scr.SetCell(x, y, cell)
}
}
+
+ if textOnly {
+ // Make sure we add a newline after each line of selected text
+ selectedText.WriteByte('\n')
+ }
+ }
+
+ if textOnly {
+ return strings.TrimSpace(selectedText.String())
}
return scr.Render()
}
+// View implements List.
+func (l *list[T]) View() string {
+ if l.height <= 0 || l.width <= 0 {
+ return ""
+ }
+ t := styles.CurrentTheme()
+ view := l.rendered
+ lines := strings.Split(view, "\n")
+
+ start, end := l.viewPosition()
+ viewStart := max(0, start)
+ viewEnd := min(len(lines), end+1)
+ lines = lines[viewStart:viewEnd]
+ if l.resize {
+ return strings.Join(lines, "\n")
+ }
+ view = t.S().Base.
+ Height(l.height).
+ Width(l.width).
+ Render(strings.Join(lines, "\n"))
+
+ if !l.hasSelection() {
+ return view
+ }
+
+ return l.selectionView(view, false)
+}
+
func (l *list[T]) viewPosition() (int, int) {
start, end := 0, 0
renderedLines := lipgloss.Height(l.rendered) - 1
@@ -1374,69 +1409,16 @@ func (l *list[T]) SelectParagraph(col, line int) {
l.selectionActive = false // Not actively selecting, just selected
}
+// HasSelection returns whether there is an active selection.
+func (l *list[T]) HasSelection() bool {
+ return l.hasSelection()
+}
+
// GetSelectedText returns the currently selected text.
func (l *list[T]) GetSelectedText(paddingLeft int) string {
- return ""
- // if !l.hasSelection() {
- // return ""
- // }
- //
- // startLine := l.selectionStartLine
- // endLine := l.selectionEndLine
- // startCol := l.selectionStartCol
- // endCol := l.selectionEndCol
- //
- // if l.direction == DirectionBackward {
- // startLine = (lipgloss.Height(l.rendered) - 1) - startLine
- // endLine = (lipgloss.Height(l.rendered) - 1) - endLine
- // }
- //
- // if l.offset > 0 {
- // if l.direction == DirectionBackward {
- // startLine += l.offset
- // endLine += l.offset
- // } else {
- // startLine -= l.offset
- // endLine -= l.offset
- // }
- // }
- //
- // lines := strings.Split(l.rendered, "\n")
- //
- // if startLine < 0 || endLine < 0 || startLine >= len(lines) || endLine >= len(lines) {
- // return ""
- // }
- //
- // var result strings.Builder
- // for i := range lines {
- // lines[i] = ansi.Strip(lines[i])
- // for _, icon := range styles.SelectionIgnoreIcons {
- // lines[i] = strings.ReplaceAll(lines[i], icon, " ")
- // }
- //
- // if i == startLine {
- // if startCol < 0 || startCol >= len(lines[i]) {
- // startCol = 0
- // }
- // if startCol < paddingLeft {
- // startCol = paddingLeft
- // }
- // if i != endLine {
- // endCol = len(lines[i])
- // }
- // result.WriteString(strings.TrimRightFunc(lines[i][startCol:endCol], unicode.IsSpace))
- // } else if i > startLine && i < endLine {
- // result.WriteString(strings.TrimRightFunc(lines[i][paddingLeft:], unicode.IsSpace))
- // } else if i == endLine {
- // if endCol < 0 || endCol >= len(lines[i]) {
- // endCol = len(lines[i])
- // }
- // if endCol < paddingLeft {
- // endCol = paddingLeft
- // }
- // result.WriteString(strings.TrimRightFunc(lines[i][paddingLeft:endCol], unicode.IsSpace))
- // }
- // }
- //
- // return result.String()
+ if !l.hasSelection() {
+ return ""
+ }
+
+ return l.selectionView(l.View(), true)
}
@@ -19,6 +19,7 @@ import (
"github.com/charmbracelet/crush/internal/tui/components/chat"
"github.com/charmbracelet/crush/internal/tui/components/chat/editor"
"github.com/charmbracelet/crush/internal/tui/components/chat/header"
+ "github.com/charmbracelet/crush/internal/tui/components/chat/messages"
"github.com/charmbracelet/crush/internal/tui/components/chat/sidebar"
"github.com/charmbracelet/crush/internal/tui/components/chat/splash"
"github.com/charmbracelet/crush/internal/tui/components/completions"
@@ -172,10 +173,18 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
return p, nil
case tea.MouseClickMsg:
+ if p.isMouseOverChat(msg.X, msg.Y) {
+ p.focusedPane = PanelTypeChat
+ p.chat.Focus()
+ p.editor.Blur()
+ } else {
+ p.focusedPane = PanelTypeEditor
+ p.editor.Focus()
+ p.chat.Blur()
+ }
u, cmd := p.chat.Update(msg)
p.chat = u.(chat.MessageListCmp)
return p, cmd
- return p, nil
case tea.MouseMotionMsg:
if msg.Button == tea.MouseLeft {
u, cmd := p.chat.Update(msg)
@@ -857,10 +866,7 @@ func (p *chatPage) Help() help.KeyMap {
key.WithKeys("up", "down"),
key.WithHelp("↑↓", "scroll"),
),
- key.NewBinding(
- key.WithKeys("c", "y"),
- key.WithHelp("c/y", "copy"),
- ),
+ messages.CopyKey,
)
fullList = append(fullList,
[]key.Binding{
@@ -5,9 +5,9 @@ import (
"github.com/charmbracelet/x/exp/charmtone"
)
-func NewCrushTheme() *Theme {
+func NewCharmtoneTheme() *Theme {
t := &Theme{
- Name: "crush",
+ Name: "charmtone",
IsDark: true,
Primary: charmtone.Charple,
@@ -56,6 +56,9 @@ func NewCrushTheme() *Theme {
Cherry: charmtone.Cherry,
}
+ // Text selection.
+ t.TextSelection = lipgloss.NewStyle().Foreground(charmtone.Salt).Background(charmtone.Charple)
+
// LSP and MCP status.
t.ItemOfflineIcon = lipgloss.NewStyle().Foreground(charmtone.Squid).SetString("●")
t.ItemBusyIcon = t.ItemOfflineIcon.Foreground(charmtone.Citron)
@@ -74,6 +74,9 @@ type Theme struct {
RedLight color.Color
Cherry color.Color
+ // Text selection.
+ TextSelection lipgloss.Style
+
// LSP and MCP status indicators.
ItemOfflineIcon lipgloss.Style
ItemBusyIcon lipgloss.Style
@@ -491,26 +494,26 @@ func SetDefaultManager(m *Manager) {
func DefaultManager() *Manager {
if defaultManager == nil {
- defaultManager = NewManager("crush")
+ defaultManager = NewManager()
}
return defaultManager
}
func CurrentTheme() *Theme {
if defaultManager == nil {
- defaultManager = NewManager("crush")
+ defaultManager = NewManager()
}
return defaultManager.Current()
}
-func NewManager(defaultTheme string) *Manager {
+func NewManager() *Manager {
m := &Manager{
themes: make(map[string]*Theme),
}
- m.Register(NewCrushTheme())
-
- m.current = m.themes[defaultTheme]
+ t := NewCharmtoneTheme() // default theme
+ m.Register(t)
+ m.current = m.themes[t.Name]
return m
}