Detailed changes
@@ -1,6 +1,12 @@
module github.com/charmbracelet/crush
-go 1.24.0
+go 1.24.3
+
+replace github.com/charmbracelet/bubbletea/v2 => ../bubbletea
+
+replace github.com/charmbracelet/lipgloss/v2 => ../lipgloss
+
+replace github.com/charmbracelet/uv => ../uv
require (
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0
@@ -17,7 +23,7 @@ require (
github.com/charmbracelet/fang v0.1.0
github.com/charmbracelet/glamour/v2 v2.0.0-20250516160903-6f1e2c8f9ebe
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1.0.20250523195325-2d1af06b557c
- github.com/charmbracelet/x/ansi v0.9.3-0.20250602153603-fb931ed90413
+ github.com/charmbracelet/x/ansi v0.9.3
github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444
github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a
github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec
@@ -41,7 +47,11 @@ require (
)
require (
- golang.org/x/term v0.31.0 // indirect
+ github.com/charmbracelet/uv v0.0.0-00010101000000-000000000000 // indirect
+ github.com/charmbracelet/x/termios v0.1.1 // indirect
+)
+
+require (
cloud.google.com/go v0.116.0 // indirect
cloud.google.com/go/auth v0.13.0 // indirect
cloud.google.com/go/compute/metadata v0.6.0 // indirect
@@ -69,7 +79,6 @@ require (
github.com/charmbracelet/colorprofile v0.3.1 // indirect
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/input v0.3.5-0.20250509021451-13796e822d86 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/charmbracelet/x/windows v0.2.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
@@ -132,6 +141,7 @@ require (
golang.org/x/net v0.39.0 // indirect
golang.org/x/sync v0.13.0 // indirect
golang.org/x/sys v0.32.0 // indirect
+ golang.org/x/term v0.31.0 // indirect
golang.org/x/text v0.24.0 // indirect
google.golang.org/genai v1.3.0
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect
@@ -70,18 +70,14 @@ github.com/charlievieth/fastwalk v1.0.11 h1:5sLT/q9+d9xMdpKExawLppqvXFZCVKf6JHnr
github.com/charlievieth/fastwalk v1.0.11/go.mod h1:yGy1zbxog41ZVMcKA/i8ojXLFsuayX5VvwhQVoj9PBI=
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250607113720-eb5e1cf3b09e h1:99Ugtt633rqauFsXjZobZmtkNpeaWialfj8dl6COC6A=
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250607113720-eb5e1cf3b09e/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw=
-github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3.0.20250609143341-c76fa36f1b94 h1:QIi50k+uNTJmp2sMs+33D1m/EWr/7OPTJ8x92AY3eOc=
-github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3.0.20250609143341-c76fa36f1b94/go.mod h1:oOn1YZGZyJHxJfh4sFAna9vDzxJRNuErLETr/lnlB/I=
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.1.0 h1:SlZS2crf3/zQh7Mr4+W+7QR1k+L08rrPX5rm5z3d7Wg=
github.com/charmbracelet/fang v0.1.0/go.mod h1:Zl/zeUQ8EtQuGyiV0ZKZlZPDowKRTzu8s/367EpN/fc=
github.com/charmbracelet/glamour/v2 v2.0.0-20250516160903-6f1e2c8f9ebe h1:i6ce4CcAlPpTj2ER69m1DBeLZ3RRcHnKExuwhKa3GfY=
github.com/charmbracelet/glamour/v2 v2.0.0-20250516160903-6f1e2c8f9ebe/go.mod h1:p3Q+aN4eQKeM5jhrmXPMgPrlKbmc59rWSnMsSA3udhk=
-github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1.0.20250523195325-2d1af06b557c h1:177KMz8zHRlEZJsWzafbKYh6OdjgvTspoH+UjaxgIXY=
-github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1.0.20250523195325-2d1af06b557c/go.mod h1:EJWvaCrhOhNGVZMvcjc0yVryl4qqpMs8tz0r9WyEkdQ=
-github.com/charmbracelet/x/ansi v0.9.3-0.20250602153603-fb931ed90413 h1:L07QkDqRF274IZ2UJ/mCTL8DR95efU9BNWLYCDXEjvQ=
-github.com/charmbracelet/x/ansi v0.9.3-0.20250602153603-fb931ed90413/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
+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/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-20250603201427-c31516f43444 h1:IJDiTgVE56gkAGfq0lBEloWgkXMk4hl/bmuPoicI4R0=
@@ -90,10 +86,10 @@ github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a h1:FsHE
github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/exp/slice v0.0.0-20250611152503-f53cdd7e01ef h1:v7qwsZ2OxzlwvpKwz8dtZXp7fIJlcDEUOyFBNE4fz4Q=
github.com/charmbracelet/x/exp/slice v0.0.0-20250611152503-f53cdd7e01ef/go.mod h1:vI5nDVMWi6veaYH+0Fmvpbe/+cv/iJfMntdh+N0+Tms=
-github.com/charmbracelet/x/input v0.3.5-0.20250509021451-13796e822d86 h1:BxAEmOBIDajkgao3EsbBxKQCYvgYPGdT62WASLvtf4Y=
-github.com/charmbracelet/x/input v0.3.5-0.20250509021451-13796e822d86/go.mod h1:62Rp/6EtTxoeJDSdtpA3tJp3y3ZRpsiekBSje+K8htA=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
+github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
+github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
github.com/charmbracelet/x/windows v0.2.1 h1:3x7vnbpQrjpuq/4L+I4gNsG5htYoCiA5oe9hLjAij5I=
github.com/charmbracelet/x/windows v0.2.1/go.mod h1:ptZp16h40gDYqs5TSawSVW+yiLB13j4kSMA0lSCHL0M=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
@@ -257,7 +257,7 @@ func (a *anim) updateChars(chars *[]cyclingChar) {
}
// View renders the animation.
-func (a anim) View() tea.View {
+func (a anim) View() string {
var (
t = styles.CurrentTheme()
b strings.Builder
@@ -289,7 +289,7 @@ func (a anim) View() tea.View {
b.WriteString(textStyle.Render(a.ellipsis.View()))
}
- return tea.NewView(b.String())
+ return b.String()
}
func GetColor(c color.Color) string {
@@ -101,12 +101,10 @@ func (m *messageListCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
// View renders the message list or an initial screen if empty.
-func (m *messageListCmp) View() tea.View {
- return tea.NewView(
- lipgloss.JoinVertical(
- lipgloss.Left,
- m.listCmp.View().String(),
- ),
+func (m *messageListCmp) View() string {
+ return lipgloss.JoinVertical(
+ lipgloss.Left,
+ m.listCmp.View(),
)
}
@@ -265,20 +265,22 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, tea.Batch(cmds...)
}
-func (m *editorCmp) View() tea.View {
- t := styles.CurrentTheme()
+func (m *editorCmp) Cursor() *tea.Cursor {
cursor := m.textarea.Cursor()
if cursor != nil {
cursor.X = cursor.X + m.x + 1
cursor.Y = cursor.Y + m.y + 1 // adjust for padding
}
+ return cursor
+}
+
+func (m *editorCmp) View() string {
+ t := styles.CurrentTheme()
if len(m.attachments) == 0 {
content := t.S().Base.Padding(1).Render(
m.textarea.View(),
)
- view := tea.NewView(content)
- view.SetCursor(cursor)
- return view
+ return content
}
content := t.S().Base.Padding(0, 1, 1, 1).Render(
lipgloss.JoinVertical(lipgloss.Top,
@@ -286,9 +288,7 @@ func (m *editorCmp) View() tea.View {
m.textarea.View(),
),
)
- view := tea.NewView(content)
- view.SetCursor(cursor)
- return view
+ return content
}
func (m *editorCmp) SetSize(width, height int) tea.Cmd {
@@ -58,9 +58,9 @@ func (p *header) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return p, nil
}
-func (p *header) View() tea.View {
+func (p *header) View() string {
if p.session.ID == "" {
- return tea.NewView("")
+ return ""
}
t := styles.CurrentTheme()
@@ -87,7 +87,7 @@ func (p *header) View() tea.View {
parts...,
),
)
- return tea.NewView(content)
+ return content
}
func (h *header) details() string {
@@ -83,20 +83,20 @@ func (m *messageCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// View renders the message component based on its current state.
// Returns different views for spinning, user, and assistant messages.
-func (m *messageCmp) View() tea.View {
+func (m *messageCmp) View() string {
if m.spinning {
- return tea.NewView(m.style().PaddingLeft(1).Render(m.anim.View().String()))
+ return m.style().PaddingLeft(1).Render(m.anim.View())
}
if m.message.ID != "" {
// this is a user or assistant message
switch m.message.Role {
case message.User:
- return tea.NewView(m.renderUserMessage())
+ return m.renderUserMessage()
default:
- return tea.NewView(m.renderAssistantMessage())
+ return m.renderAssistantMessage()
}
}
- return tea.NewView(m.style().Render("No message content"))
+ return m.style().Render("No message content")
}
// GetMessage returns the underlying message data
@@ -283,7 +283,7 @@ func (m *assistantSectionModel) Update(tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
}
-func (m *assistantSectionModel) View() tea.View {
+func (m *assistantSectionModel) View() string {
t := styles.CurrentTheme()
finishData := m.message.FinishPart()
finishTime := time.Unix(finishData.Time, 0)
@@ -292,10 +292,8 @@ func (m *assistantSectionModel) View() tea.View {
icon := t.S().Subtle.Render(styles.ModelIcon)
model := t.S().Muted.Render(models.SupportedModels[m.message.Model].Name)
assistant := fmt.Sprintf("%s %s %s", icon, model, infoMsg)
- return tea.NewView(
- t.S().Base.PaddingLeft(2).Render(
- core.Section(assistant, m.width-2),
- ),
+ return t.S().Base.PaddingLeft(2).Render(
+ core.Section(assistant, m.width-2),
)
}
@@ -542,7 +542,7 @@ func (tr agentRenderer) Render(v *toolCallCmp) string {
if v.result.ToolCallID == "" {
v.spinning = true
- parts = append(parts, v.anim.View().String())
+ parts = append(parts, v.anim.View())
} else {
v.spinning = false
}
@@ -133,19 +133,19 @@ func (m *toolCallCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// View renders the tool call component based on its current state.
// Shows either a pending animation or the tool-specific rendered result.
-func (m *toolCallCmp) View() tea.View {
+func (m *toolCallCmp) View() string {
box := m.style()
if !m.call.Finished && !m.cancelled {
- return tea.NewView(box.Render(m.renderPending()))
+ return box.Render(m.renderPending())
}
r := registry.lookup(m.call.Name)
if m.isNested {
- return tea.NewView(box.Render(r.Render(m)))
+ return box.Render(r.Render(m))
}
- return tea.NewView(box.Render(r.Render(m)))
+ return box.Render(r.Render(m))
}
// State management methods
@@ -107,7 +107,7 @@ func (m *sidebarCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
}
-func (m *sidebarCmp) View() tea.View {
+func (m *sidebarCmp) View() string {
t := styles.CurrentTheme()
parts := []string{}
if !m.compactMode {
@@ -139,9 +139,7 @@ func (m *sidebarCmp) View() tea.View {
m.mcpBlock(),
)
- return tea.NewView(
- lipgloss.JoinVertical(lipgloss.Left, parts...),
- )
+ return lipgloss.JoinVertical(lipgloss.Left, parts...)
}
func (m *sidebarCmp) handleFileHistoryEvent(event pubsub.Event[history.File]) tea.Cmd {
@@ -157,15 +157,12 @@ func (c *completionsCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
// View implements Completions.
-func (c *completionsCmp) View() tea.View {
+func (c *completionsCmp) View() string {
if len(c.list.Items()) == 0 {
- return tea.NewView(c.style().Render("No completions found"))
+ return c.style().Render("No completions found")
}
- view := tea.NewView(
- c.style().Render(c.list.View().String()),
- )
- return view
+ return c.style().Render(c.list.View())
}
func (c *completionsCmp) style() lipgloss.Style {
@@ -75,7 +75,7 @@ func (c *completionItemCmp) Update(tea.Msg) (tea.Model, tea.Cmd) {
}
// View implements CommandItem.
-func (c *completionItemCmp) View() tea.View {
+func (c *completionItemCmp) View() string {
t := styles.CurrentTheme()
itemStyle := t.S().Base.Padding(0, 1).Width(c.width)
@@ -135,7 +135,7 @@ func (c *completionItemCmp) View() tea.View {
parts...,
),
)
- return tea.NewView(item)
+ return item
}
// Blur implements CommandItem.
@@ -72,7 +72,14 @@ func (c *container) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
}
-func (c *container) View() tea.View {
+func (c *container) Cursor() *tea.Cursor {
+ if cursor, ok := c.content.(util.Cursor); ok {
+ return cursor.Cursor()
+ }
+ return nil
+}
+
+func (c *container) View() string {
t := styles.CurrentTheme()
width := c.width
height := c.height
@@ -106,10 +113,7 @@ func (c *container) View() tea.View {
PaddingLeft(c.paddingLeft)
contentView := c.content.View()
- view := tea.NewView(style.Render(contentView.String()))
- cursor := contentView.Cursor()
- view.SetCursor(cursor)
- return view
+ return style.Render(contentView)
}
func (c *container) SetSize(width, height int) tea.Cmd {
@@ -104,17 +104,34 @@ func (s *splitPaneLayout) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return s, tea.Batch(cmds...)
}
-func (s *splitPaneLayout) View() tea.View {
+func (s *splitPaneLayout) Cursor() *tea.Cursor {
+ if s.bottomPanel != nil {
+ if c, ok := s.bottomPanel.(util.Cursor); ok {
+ return c.Cursor()
+ }
+ } else if s.rightPanel != nil {
+ if c, ok := s.rightPanel.(util.Cursor); ok {
+ return c.Cursor()
+ }
+ } else if s.leftPanel != nil {
+ if c, ok := s.leftPanel.(util.Cursor); ok {
+ return c.Cursor()
+ }
+ }
+ return nil
+}
+
+func (s *splitPaneLayout) View() string {
var topSection string
if s.leftPanel != nil && s.rightPanel != nil {
leftView := s.leftPanel.View()
rightView := s.rightPanel.View()
- topSection = lipgloss.JoinHorizontal(lipgloss.Top, leftView.String(), rightView.String())
+ topSection = lipgloss.JoinHorizontal(lipgloss.Top, leftView, rightView)
} else if s.leftPanel != nil {
- topSection = s.leftPanel.View().String()
+ topSection = s.leftPanel.View()
} else if s.rightPanel != nil {
- topSection = s.rightPanel.View().String()
+ topSection = s.rightPanel.View()
} else {
topSection = ""
}
@@ -123,32 +140,20 @@ func (s *splitPaneLayout) View() tea.View {
if s.bottomPanel != nil && topSection != "" {
bottomView := s.bottomPanel.View()
- finalView = lipgloss.JoinVertical(lipgloss.Left, topSection, bottomView.String())
+ finalView = lipgloss.JoinVertical(lipgloss.Left, topSection, bottomView)
} else if s.bottomPanel != nil {
- finalView = s.bottomPanel.View().String()
+ finalView = s.bottomPanel.View()
} else {
finalView = topSection
}
- // TODO: think of a better way to handle multiple cursors
- var cursor *tea.Cursor
- if s.bottomPanel != nil {
- cursor = s.bottomPanel.View().Cursor()
- } else if s.rightPanel != nil {
- cursor = s.rightPanel.View().Cursor()
- } else if s.leftPanel != nil {
- cursor = s.leftPanel.View().Cursor()
- }
-
t := styles.CurrentTheme()
style := t.S().Base.
Width(s.width).
Height(s.height)
- view := tea.NewView(style.Render(finalView))
- view.SetCursor(cursor)
- return view
+ return style.Render(finalView)
}
func (s *splitPaneLayout) SetSize(width, height int) tea.Cmd {
@@ -281,12 +281,20 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
}
+// Cursor returns the current cursor position in the input field.
+func (m *model) Cursor() *tea.Cursor {
+ if m.filterable && !m.hideFilterInput {
+ return m.input.Cursor()
+ }
+ return nil
+}
+
// View renders the list to a string for display.
// Returns empty string if the list has no dimensions.
// Triggers re-rendering if needed before returning content.
-func (m *model) View() tea.View {
+func (m *model) View() string {
if m.viewState.height == 0 || m.viewState.width == 0 {
- return tea.NewView("") // No content to display
+ return "" // No content to display
}
if m.renderState.needsRerender {
m.renderVisible()
@@ -304,11 +312,7 @@ func (m *model) View() tea.View {
content,
)
}
- view := tea.NewView(content)
- if m.filterable && !m.hideFilterInput {
- view.SetCursor(m.input.Cursor())
- }
- return view
+ return content
}
// handleKeyPress processes keyboard input for list navigation.
@@ -834,7 +838,7 @@ func (m *model) rerenderItem(inx int) {
func (m *model) getItemLines(item util.Model) []string {
var itemLines []string
- itemLines = strings.Split(item.View().String(), "\n")
+ itemLines = strings.Split(item.View(), "\n")
if m.gapSize > 0 {
gap := make([]string, m.gapSize)
@@ -1262,7 +1266,7 @@ func (m *model) filterSection(sect section, search string) *section {
// Check if section header itself matches
if sect.header != nil {
- headerText := strings.ToLower(sect.header.View().String())
+ headerText := strings.ToLower(sect.header.View())
if strings.Contains(headerText, search) {
hasHeaderMatch = true
// If header matches, include all items in the section
@@ -94,13 +94,13 @@ func (m *statusCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
}
-func (m *statusCmp) View() tea.View {
+func (m *statusCmp) View() string {
t := styles.CurrentTheme()
status := t.S().Base.Padding(0, 1, 1, 1).Render(m.help.View(m.keyMap))
if m.info.Msg != "" {
status = m.infoMsg()
}
- return tea.NewView(status)
+ return status
}
func (m *statusCmp) infoMsg() string {
@@ -139,7 +139,7 @@ func (c *commandArgumentsDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
// View implements CommandArgumentsDialog.
-func (c *commandArgumentsDialogCmp) View() tea.View {
+func (c *commandArgumentsDialogCmp) View() string {
t := styles.CurrentTheme()
baseStyle := t.S().Base
@@ -188,19 +188,19 @@ func (c *commandArgumentsDialogCmp) View() tea.View {
elements...,
)
- view := tea.NewView(
- baseStyle.Padding(1, 1, 0, 1).
- Border(lipgloss.RoundedBorder()).
- BorderForeground(t.BorderFocus).
- Width(c.width).
- Render(content),
- )
+ return baseStyle.Padding(1, 1, 0, 1).
+ Border(lipgloss.RoundedBorder()).
+ BorderForeground(t.BorderFocus).
+ Width(c.width).
+ Render(content)
+}
+
+func (c *commandArgumentsDialogCmp) Cursor() *tea.Cursor {
cursor := c.inputs[c.focusIndex].Cursor()
if cursor != nil {
cursor = c.moveCursor(cursor)
}
- view.SetCursor(cursor)
- return view
+ return cursor
}
func (c *commandArgumentsDialogCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor {
@@ -143,23 +143,29 @@ func (c *commandDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return c, nil
}
-func (c *commandDialogCmp) View() tea.View {
+func (c *commandDialogCmp) View() string {
t := styles.CurrentTheme()
- listView := c.commandList.View()
+ listView := c.commandList
radio := c.commandTypeRadio()
content := lipgloss.JoinVertical(
lipgloss.Left,
t.S().Base.Padding(0, 1, 1, 1).Render(core.Title("Commands", c.width-lipgloss.Width(radio)-5)+" "+radio),
- listView.String(),
+ listView.View(),
"",
t.S().Base.Width(c.width-2).PaddingLeft(1).AlignHorizontal(lipgloss.Left).Render(c.help.View(c.keyMap)),
)
- v := tea.NewView(c.style().Render(content))
- if listView.Cursor() != nil {
- c := c.moveCursor(listView.Cursor())
- v.SetCursor(c)
+ return c.style().Render(content)
+}
+
+func (c *commandDialogCmp) Cursor() *tea.Cursor {
+ if cursor, ok := c.commandList.(util.Cursor); ok {
+ cursor := cursor.Cursor()
+ if cursor != nil {
+ cursor = c.moveCursor(cursor)
+ }
+ return cursor
}
- return v
+ return nil
}
func (c *commandDialogCmp) commandTypeRadio() string {
@@ -35,12 +35,12 @@ func (m *itemSectionModel) Update(tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
}
-func (m *itemSectionModel) View() tea.View {
+func (m *itemSectionModel) View() string {
t := styles.CurrentTheme()
title := ansi.Truncate(m.title, m.width-2, "…")
style := t.S().Base.Padding(1, 1, 0, 1)
title = t.S().Muted.Render(title)
- return tea.NewView(style.Render(core.Section(title, m.width-2)))
+ return style.Render(core.Section(title, m.width-2))
}
func (m *itemSectionModel) GetSize() (int, int) {
@@ -242,8 +242,8 @@ func (c *compactDialogCmp) render() string {
Render(dialogContent)
}
-func (c *compactDialogCmp) View() tea.View {
- return tea.NewView(c.render())
+func (c *compactDialogCmp) View() string {
+ return c.render()
}
// SetSize sets the size of the component.
@@ -37,7 +37,7 @@ type DialogCmp interface {
Dialogs() []DialogModel
HasDialogs() bool
GetLayers() []*lipgloss.Layer
- ActiveView() *tea.View
+ ActiveModel() util.Model
ActiveDialogID() DialogID
}
@@ -132,12 +132,11 @@ func (d dialogCmp) Dialogs() []DialogModel {
return d.dialogs
}
-func (d dialogCmp) ActiveView() *tea.View {
+func (d dialogCmp) ActiveModel() util.Model {
if len(d.dialogs) == 0 {
return nil
}
- view := d.dialogs[len(d.dialogs)-1].View()
- return &view
+ return d.dialogs[len(d.dialogs)-1]
}
func (d dialogCmp) ActiveDialogID() DialogID {
@@ -150,7 +149,7 @@ func (d dialogCmp) ActiveDialogID() DialogID {
func (d dialogCmp) GetLayers() []*lipgloss.Layer {
layers := []*lipgloss.Layer{}
for _, dialog := range d.Dialogs() {
- dialogView := dialog.View().String()
+ dialogView := dialog.View()
row, col := dialog.Position()
layers = append(layers, lipgloss.NewLayer(dialogView).X(col).Y(row))
}
@@ -148,7 +148,7 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, tea.Batch(cmds...)
}
-func (m *model) View() tea.View {
+func (m *model) View() string {
t := styles.CurrentTheme()
content := lipgloss.JoinVertical(
@@ -158,7 +158,7 @@ func (m *model) View() tea.View {
m.filePicker.View(),
t.S().Base.Width(m.width-2).PaddingLeft(1).AlignHorizontal(lipgloss.Left).Render(m.help.View(m.keyMap)),
)
- return tea.NewView(m.style().Render(content))
+ return m.style().Render(content)
}
func (m *model) currentImage() string {
@@ -147,8 +147,8 @@ func (m *initDialogCmp) render() string {
}
// View implements tea.Model.
-func (m *initDialogCmp) View() tea.View {
- return tea.NewView(m.render())
+func (m *initDialogCmp) View() string {
+ return m.render()
}
// SetSize sets the size of the component.
@@ -154,22 +154,28 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
}
-func (m *modelDialogCmp) View() tea.View {
+func (m *modelDialogCmp) View() string {
t := styles.CurrentTheme()
listView := m.modelList.View()
content := lipgloss.JoinVertical(
lipgloss.Left,
t.S().Base.Padding(0, 1, 1, 1).Render(core.Title("Switch Model", m.width-4)),
- listView.String(),
+ listView,
"",
t.S().Base.Width(m.width-2).PaddingLeft(1).AlignHorizontal(lipgloss.Left).Render(m.help.View(m.keyMap)),
)
- v := tea.NewView(m.style().Render(content))
- if listView.Cursor() != nil {
- c := m.moveCursor(listView.Cursor())
- v.SetCursor(c)
+ return m.style().Render(content)
+}
+
+func (m *modelDialogCmp) Cursor() *tea.Cursor {
+ if cursor, ok := m.modelList.(util.Cursor); ok {
+ cursor := cursor.Cursor()
+ if cursor != nil {
+ cursor = m.moveCursor(cursor)
+ return cursor
+ }
}
- return v
+ return nil
}
func (m *modelDialogCmp) style() lipgloss.Style {
@@ -478,8 +478,8 @@ func (p *permissionDialogCmp) render() string {
)
}
-func (p *permissionDialogCmp) View() tea.View {
- return tea.NewView(p.render())
+func (p *permissionDialogCmp) View() string {
+ return p.render()
}
func (p *permissionDialogCmp) SetSize() tea.Cmd {
@@ -65,7 +65,7 @@ func (q *quitDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
// View renders the quit dialog with Yes/No buttons.
-func (q *quitDialogCmp) View() tea.View {
+func (q *quitDialogCmp) View() string {
t := styles.CurrentTheme()
baseStyle := t.S().Base
yesStyle := t.S().Text
@@ -100,9 +100,7 @@ func (q *quitDialogCmp) View() tea.View {
Border(lipgloss.RoundedBorder()).
BorderForeground(t.BorderFocus)
- return tea.NewView(
- quitDialogStyle.Render(content),
- )
+ return quitDialogStyle.Render(content)
}
func (q *quitDialogCmp) Position() (int, int) {
@@ -122,23 +122,29 @@ func (s *sessionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return s, nil
}
-func (s *sessionDialogCmp) View() tea.View {
+func (s *sessionDialogCmp) View() string {
t := styles.CurrentTheme()
listView := s.sessionsList.View()
content := lipgloss.JoinVertical(
lipgloss.Left,
t.S().Base.Padding(0, 1, 1, 1).Render(core.Title("Switch Session", s.width-4)),
- listView.String(),
+ listView,
"",
t.S().Base.Width(s.width-2).PaddingLeft(1).AlignHorizontal(lipgloss.Left).Render(s.help.View(s.keyMap)),
)
- v := tea.NewView(s.style().Render(content))
- if listView.Cursor() != nil {
- c := s.moveCursor(listView.Cursor())
- v.SetCursor(c)
+ return s.style().Render(content)
+}
+
+func (s *sessionDialogCmp) Cursor() *tea.Cursor {
+ if cursor, ok := s.sessionsList.(util.Cursor); ok {
+ cursor := cursor.Cursor()
+ if cursor != nil {
+ cursor = s.moveCursor(cursor)
+ }
+ return cursor
}
- return v
+ return nil
}
func (s *sessionDialogCmp) style() lipgloss.Style {
@@ -145,7 +145,7 @@ func getRelativeTime(logTime time.Time) string {
}
}
-func (i *detailCmp) View() tea.View {
+func (i *detailCmp) View() string {
t := styles.CurrentTheme()
style := t.S().Base.
BorderStyle(lipgloss.RoundedBorder()).
@@ -153,7 +153,7 @@ func (i *detailCmp) View() tea.View {
Width(i.width - 2). // Adjust width for border
Height(i.height - 2). // Adjust height for border
Padding(1)
- return tea.NewView(style.Render(i.viewport.View()))
+ return style.Render(i.viewport.View())
}
func (i *detailCmp) GetSize() (int, int) {
@@ -74,7 +74,7 @@ func (i *tableCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return i, tea.Batch(cmds...)
}
-func (i *tableCmp) View() tea.View {
+func (i *tableCmp) View() string {
t := styles.CurrentTheme()
defaultStyles := table.DefaultStyles()
@@ -97,7 +97,7 @@ func (i *tableCmp) View() tea.View {
Foreground(t.FgBase)
i.table.SetStyles(defaultStyles)
- return tea.NewView(i.table.View())
+ return i.table.View()
}
func (i *tableCmp) GetSize() (int, int) {
@@ -306,7 +306,7 @@ func (p *chatPage) GetSize() (int, int) {
return p.layout.GetSize()
}
-func (p *chatPage) View() tea.View {
+func (p *chatPage) View() string {
if !p.compactMode || p.session.ID == "" {
// If not in compact mode or there is no session, we don't show the header
return p.layout.View()
@@ -314,8 +314,8 @@ func (p *chatPage) View() tea.View {
layoutView := p.layout.View()
chatView := strings.Join(
[]string{
- p.header.View().String(),
- layoutView.String(),
+ p.header.View(),
+ layoutView,
}, "\n",
)
layers := []*lipgloss.Layer{
@@ -330,7 +330,7 @@ func (p *chatPage) View() tea.View {
details := style.Render(
lipgloss.JoinVertical(
lipgloss.Left,
- p.compactSidebar.View().String(),
+ p.compactSidebar.View(),
version,
),
)
@@ -339,9 +339,14 @@ func (p *chatPage) View() tea.View {
canvas := lipgloss.NewCanvas(
layers...,
)
- view := tea.NewView(canvas.Render())
- view.SetCursor(layoutView.Cursor())
- return view
+ return canvas.Render()
+}
+
+func (p *chatPage) Cursor() *tea.Cursor {
+ if v, ok := p.layout.(util.Cursor); ok {
+ return v.Cursor()
+ }
+ return nil
}
func (p *chatPage) Bindings() []key.Binding {
@@ -51,18 +51,16 @@ func (p *logsPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return p, tea.Batch(cmds...)
}
-func (p *logsPage) View() tea.View {
+func (p *logsPage) View() string {
baseStyle := styles.CurrentTheme().S().Base
style := baseStyle.Width(p.width).Height(p.height).Padding(1)
title := core.Title("Logs", p.width-2)
- return tea.NewView(
- style.Render(
- lipgloss.JoinVertical(lipgloss.Top,
- title,
- p.details.View().String(),
- p.table.View().String(),
- ),
+ return style.Render(
+ lipgloss.JoinVertical(lipgloss.Top,
+ title,
+ p.details.View(),
+ p.table.View(),
),
)
}
@@ -84,6 +84,9 @@ func (a appModel) Init() tea.Cmd {
return nil
})
+ // Enable mouse support.
+ cmds = append(cmds, tea.EnableMouseAllMotion)
+
return tea.Batch(cmds...)
}
@@ -391,9 +394,9 @@ func (a *appModel) View() tea.View {
a.status.SetKeyMap(a.keyMap)
pageView := page.View()
components := []string{
- pageView.String(),
+ pageView,
}
- components = append(components, a.status.View().String())
+ components = append(components, a.status.View())
appView := lipgloss.JoinVertical(lipgloss.Top, components...)
layers := []*lipgloss.Layer{
@@ -406,14 +409,20 @@ func (a *appModel) View() tea.View {
)
}
- cursor := pageView.Cursor()
- activeView := a.dialog.ActiveView()
+ var cursor *tea.Cursor
+ if v, ok := page.(util.Cursor); ok {
+ cursor = v.Cursor()
+ }
+ activeView := a.dialog.ActiveModel()
if activeView != nil {
- cursor = activeView.Cursor()
+ cursor = nil // Reset cursor if a dialog is active unless it implements util.Cursor
+ if v, ok := activeView.(util.Cursor); ok {
+ cursor = v.Cursor()
+ }
}
if a.completions.Open() && cursor != nil {
- cmp := a.completions.View().String()
+ cmp := a.completions.View()
x, y := a.completions.Position()
layers = append(
layers,
@@ -425,10 +434,11 @@ func (a *appModel) View() tea.View {
layers...,
)
+ var view tea.View
t := styles.CurrentTheme()
- view := tea.NewView(canvas.Render())
- view.SetBackgroundColor(t.BgBase)
- view.SetCursor(cursor)
+ view.Layer = canvas
+ view.BackgroundColor = t.BgBase
+ view.Cursor = cursor
return view
}
@@ -6,9 +6,13 @@ import (
tea "github.com/charmbracelet/bubbletea/v2"
)
+type Cursor interface {
+ Cursor() *tea.Cursor
+}
+
type Model interface {
tea.Model
- tea.Viewable
+ tea.ViewModel
}
func CmdHandler(msg tea.Msg) tea.Cmd {