diff --git a/go.mod b/go.mod index f7c8f4d1c11ea2bd7bcc747c94ddab224a63a7bf..b04192434c8e237bf0f891645124f916e867e5e9 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 56284da1290cd092f24115a9a71084564d62284d..bf853b8f3a36ed6142098cf92e3824eb15e7ba1a 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/tui/components/anim/anim.go b/internal/tui/components/anim/anim.go index 8d40bbfe03e775de779414d60c94ba3e6f80635f..5ab75676775acfba7dbe606d01bd049c39dcf013 100644 --- a/internal/tui/components/anim/anim.go +++ b/internal/tui/components/anim/anim.go @@ -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 { diff --git a/internal/tui/components/chat/chat.go b/internal/tui/components/chat/chat.go index 21e7b74cdcf569c28a12a647069774a0c255715c..058e3612d31f0e0d6be53fe28c4f8720fa49cb5b 100644 --- a/internal/tui/components/chat/chat.go +++ b/internal/tui/components/chat/chat.go @@ -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(), ) } diff --git a/internal/tui/components/chat/editor/editor.go b/internal/tui/components/chat/editor/editor.go index 04fbc2e22b1e49adc77b3be88c252b88b29c59a4..e0c1637c13dcd4fd8b5ae0bd137044130a6a4a42 100644 --- a/internal/tui/components/chat/editor/editor.go +++ b/internal/tui/components/chat/editor/editor.go @@ -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 { diff --git a/internal/tui/components/chat/header/header.go b/internal/tui/components/chat/header/header.go index 78620161a75a3ade2e0e2416351c50699ac8bd4d..977c2700020999e5b56daec9a165d2b9b1ea06ef 100644 --- a/internal/tui/components/chat/header/header.go +++ b/internal/tui/components/chat/header/header.go @@ -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 { diff --git a/internal/tui/components/chat/messages/messages.go b/internal/tui/components/chat/messages/messages.go index d5e95b4e3ebded500f73840fda483d3be53ca71d..b8a43f77fdfa24fb5f5389159f5fac2606438f51 100644 --- a/internal/tui/components/chat/messages/messages.go +++ b/internal/tui/components/chat/messages/messages.go @@ -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), ) } diff --git a/internal/tui/components/chat/messages/renderer.go b/internal/tui/components/chat/messages/renderer.go index 54bdd4c84ef4a7914e16d994e94ed84158d64f4e..11009999ff82dcdde7bec20c7445b9869a5c4b62 100644 --- a/internal/tui/components/chat/messages/renderer.go +++ b/internal/tui/components/chat/messages/renderer.go @@ -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 } diff --git a/internal/tui/components/chat/messages/tool.go b/internal/tui/components/chat/messages/tool.go index 5a70acfe297e8f9716e229cb013160a6662a5970..88508c66762225c1b82db8bc208144d70e1c77a5 100644 --- a/internal/tui/components/chat/messages/tool.go +++ b/internal/tui/components/chat/messages/tool.go @@ -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 diff --git a/internal/tui/components/chat/sidebar/sidebar.go b/internal/tui/components/chat/sidebar/sidebar.go index afd067adc06d99c3c9da911812750631423231e6..5673165fab92c85d8832c09d662bccb7e7be3391 100644 --- a/internal/tui/components/chat/sidebar/sidebar.go +++ b/internal/tui/components/chat/sidebar/sidebar.go @@ -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 { diff --git a/internal/tui/components/completions/completions.go b/internal/tui/components/completions/completions.go index a039e9e86be9e6635ee2d1a1063d9e1c469dbabd..6153a76834ff697546e0c3ba38dece817bb97921 100644 --- a/internal/tui/components/completions/completions.go +++ b/internal/tui/components/completions/completions.go @@ -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 { diff --git a/internal/tui/components/completions/item.go b/internal/tui/components/completions/item.go index 12e14b48edffddb2043c03da61c956bdac2ab242..d1b18a75ba1591a52524713d228a7f8b24fa1c96 100644 --- a/internal/tui/components/completions/item.go +++ b/internal/tui/components/completions/item.go @@ -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. diff --git a/internal/tui/components/core/layout/container.go b/internal/tui/components/core/layout/container.go index 12148f9a5b134acbd9e015ec488ede573fb0a6ef..9940a320e8c3a2733c8a543e09d5c25b68a103d1 100644 --- a/internal/tui/components/core/layout/container.go +++ b/internal/tui/components/core/layout/container.go @@ -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 { diff --git a/internal/tui/components/core/layout/split.go b/internal/tui/components/core/layout/split.go index 5309091a96c8ba29d31f5432b3a761e588e698d4..cdd2d1cf72724ce0b8ef1658aedc84c67438dafb 100644 --- a/internal/tui/components/core/layout/split.go +++ b/internal/tui/components/core/layout/split.go @@ -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 { diff --git a/internal/tui/components/core/list/list.go b/internal/tui/components/core/list/list.go index 3ad3f68dffd46960bcfb5f969991f25218494a2c..8d0f93d425f4167c75188ec45a49d12e3078a351 100644 --- a/internal/tui/components/core/list/list.go +++ b/internal/tui/components/core/list/list.go @@ -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 diff --git a/internal/tui/components/core/status/status.go b/internal/tui/components/core/status/status.go index bded453e78ecdfd85d6d182b4785a55d641dfd44..c2565bc7afb4a0eb18979acbd683416c648937a2 100644 --- a/internal/tui/components/core/status/status.go +++ b/internal/tui/components/core/status/status.go @@ -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 { diff --git a/internal/tui/components/dialogs/commands/arguments.go b/internal/tui/components/dialogs/commands/arguments.go index 7e4bdcc271c0dcbdf1923c773f204c14a0fbf32b..03110eeaf2b8fbb909f1f9e4fbd57344699732e3 100644 --- a/internal/tui/components/dialogs/commands/arguments.go +++ b/internal/tui/components/dialogs/commands/arguments.go @@ -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 { diff --git a/internal/tui/components/dialogs/commands/commands.go b/internal/tui/components/dialogs/commands/commands.go index cbbbc989942268ebd2ac7c44a35a327cec1509fd..140996fdd59af21e27e6eb4017ca5cca847cb0d9 100644 --- a/internal/tui/components/dialogs/commands/commands.go +++ b/internal/tui/components/dialogs/commands/commands.go @@ -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 { diff --git a/internal/tui/components/dialogs/commands/item.go b/internal/tui/components/dialogs/commands/item.go index a89a884472cff75a0051d89b637ae4f55feba527..97668694e61c5c89e38b9034a249b6cd4a46f37e 100644 --- a/internal/tui/components/dialogs/commands/item.go +++ b/internal/tui/components/dialogs/commands/item.go @@ -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) { diff --git a/internal/tui/components/dialogs/compact/compact.go b/internal/tui/components/dialogs/compact/compact.go index 895053279ff916b113051aca3eeb1652ec82936e..86455e3139b4d0eb43baaf509b0fa0e039dd4939 100644 --- a/internal/tui/components/dialogs/compact/compact.go +++ b/internal/tui/components/dialogs/compact/compact.go @@ -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. diff --git a/internal/tui/components/dialogs/dialogs.go b/internal/tui/components/dialogs/dialogs.go index 421d946517f2111173c6fc93dc474a6374c2a326..99e14e51fdd271a9cee0c27528c7608ea28fa24e 100644 --- a/internal/tui/components/dialogs/dialogs.go +++ b/internal/tui/components/dialogs/dialogs.go @@ -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)) } diff --git a/internal/tui/components/dialogs/filepicker/filepicker.go b/internal/tui/components/dialogs/filepicker/filepicker.go index 916209b6f6371b7c5961f9fbc507f9c680f9e59b..268044d6c3fc9f9042ad29bbfc267767f5f966e1 100644 --- a/internal/tui/components/dialogs/filepicker/filepicker.go +++ b/internal/tui/components/dialogs/filepicker/filepicker.go @@ -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 { diff --git a/internal/tui/components/dialogs/init/init.go b/internal/tui/components/dialogs/init/init.go index 74d0dc0b3d9d4630b28c4b240fb17fbe611ba21f..66e26bd99f86e5b49fca11ca3daca2f74d5b8656 100644 --- a/internal/tui/components/dialogs/init/init.go +++ b/internal/tui/components/dialogs/init/init.go @@ -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. diff --git a/internal/tui/components/dialogs/models/models.go b/internal/tui/components/dialogs/models/models.go index 906d87a9dfe65c1ec09bd5abaf4f9d6865545038..57b41501477278cc738d8d3528d0c9137fe10929 100644 --- a/internal/tui/components/dialogs/models/models.go +++ b/internal/tui/components/dialogs/models/models.go @@ -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 { diff --git a/internal/tui/components/dialogs/permissions/permissions.go b/internal/tui/components/dialogs/permissions/permissions.go index b79dff8fa6b9fe8d40c461c84f9d140487816723..e7f6dd517b1504e2f938c9310e95b8a7086cbbf0 100644 --- a/internal/tui/components/dialogs/permissions/permissions.go +++ b/internal/tui/components/dialogs/permissions/permissions.go @@ -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 { diff --git a/internal/tui/components/dialogs/quit/quit.go b/internal/tui/components/dialogs/quit/quit.go index e6de718d26d62fcf481a66f670bd23570e609f39..90f2b278d60cd67f04e79f31b96c610656ad3708 100644 --- a/internal/tui/components/dialogs/quit/quit.go +++ b/internal/tui/components/dialogs/quit/quit.go @@ -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) { diff --git a/internal/tui/components/dialogs/sessions/sessions.go b/internal/tui/components/dialogs/sessions/sessions.go index 78a834911f04bbb0c1cbe100c150da4818c52da6..a95ae0c5ce9b07d499d4f78834a69ccd7ed5635f 100644 --- a/internal/tui/components/dialogs/sessions/sessions.go +++ b/internal/tui/components/dialogs/sessions/sessions.go @@ -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 { diff --git a/internal/tui/components/logs/details.go b/internal/tui/components/logs/details.go index 46ec1ce02d1e4573c04f99f45e1af2a2a3a4a731..f9a8a0bd3b857847cf8a80a1ebc8134bdac94530 100644 --- a/internal/tui/components/logs/details.go +++ b/internal/tui/components/logs/details.go @@ -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) { diff --git a/internal/tui/components/logs/table.go b/internal/tui/components/logs/table.go index 88160dc875d896a61ecd09560b74d7993f11f020..a4a8ec3c5f157576b8db7d31f38b31abcdfa5f24 100644 --- a/internal/tui/components/logs/table.go +++ b/internal/tui/components/logs/table.go @@ -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) { diff --git a/internal/tui/page/chat/chat.go b/internal/tui/page/chat/chat.go index ffb6debb0f61cb1fcfa7e180b042b3b8325dd2e5..3504b26609b626959f9b96e99ef35c3c3ab1dcfd 100644 --- a/internal/tui/page/chat/chat.go +++ b/internal/tui/page/chat/chat.go @@ -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 { diff --git a/internal/tui/page/logs/logs.go b/internal/tui/page/logs/logs.go index e8b1077d80094ee5974debc5c09f1f82562e334c..01a50d289f06bb81c88ec29a75c1592296dba754 100644 --- a/internal/tui/page/logs/logs.go +++ b/internal/tui/page/logs/logs.go @@ -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(), ), ) } diff --git a/internal/tui/tui.go b/internal/tui/tui.go index c6dee6532993becfbda24d115b8e1e5d05e4fd60..fd12c4c0e35fdb7ac692afb3bdd66ecfa854e77b 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -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 } diff --git a/internal/tui/util/util.go b/internal/tui/util/util.go index 8f7bb1bed15c184121cf5c0b16d9ba0cd98eb531..d737acb3f06a155ab52cc7eed7d32a634d85d582 100644 --- a/internal/tui/util/util.go +++ b/internal/tui/util/util.go @@ -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 {