diff --git a/go.mod b/go.mod index cbfaa3077007fce91f5c7602c77c7f2353a4682a..43687e0a10975f7c8573eaf50e2dedbd56d6fa7d 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( charm.land/bubbles/v2 v2.0.0-rc.1 charm.land/bubbletea/v2 v2.0.0-rc.2.0.20251202162339-5fa38b798f16 charm.land/fantasy v0.5.1 - charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251119143523-0334bb4562ca + charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251202164103-ad876c4132d6 charm.land/x/vcr v0.1.1 github.com/JohannesKaufmann/html-to-markdown v1.6.0 github.com/MakeNowJust/heredoc v1.0.0 @@ -21,7 +21,7 @@ require ( github.com/charmbracelet/fang v0.4.4 github.com/charmbracelet/glamour/v2 v2.0.0-20251106195642-800eb8175930 github.com/charmbracelet/log/v2 v2.0.0-20251106192421-eb64aaa963a0 - github.com/charmbracelet/ultraviolet v0.0.0-20251202162030-ecc8c1ae4b2b + github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318 github.com/charmbracelet/x/ansi v0.11.2 github.com/charmbracelet/x/exp/charmtone v0.0.0-20250708181618-a60a724ba6c3 github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f diff --git a/go.sum b/go.sum index 238b46788328c5ba0ada41bc882fd0d40949f09e..22e14d6cf4ff87fa9704cfd6b4a249b9eedbbda5 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ charm.land/bubbletea/v2 v2.0.0-rc.2.0.20251202162339-5fa38b798f16 h1:9iVAss7WF8A charm.land/bubbletea/v2 v2.0.0-rc.2.0.20251202162339-5fa38b798f16/go.mod h1:Vsh7/MLC7LQ2Ab8H63SXm6yD/L6o4HDvhdD/IrIRXrU= charm.land/fantasy v0.5.1 h1:Svi/UpI4/DwVjTqNYceDXoJJYn6SVEM5dnLH92UBiEs= charm.land/fantasy v0.5.1/go.mod h1:SPOsnIlkBKnhw2Wnasv+wZ82EmCMIGesx0je3tgR6+M= -charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251119143523-0334bb4562ca h1:6bVc8OFotCS4sS7HKqxTudP7yn8Y0ODR6df2pdlY/+s= -charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251119143523-0334bb4562ca/go.mod h1:XSJjv7DaH4zd1Y27kZis295RkEj9OFR9zh2WffQQsKQ= +charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251202164103-ad876c4132d6 h1:7gwwAGBZwCS3c8dX7OVKQJbR/SpL75Ad7JUy2bEhNjg= +charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251202164103-ad876c4132d6/go.mod h1:0H6aOGRnJHcF0owqZQFIyLZULTVXBxEvLSbouQ7BSb0= charm.land/x/vcr v0.1.1 h1:PXCFMUG0rPtyk35rhfzYCJEduOzWXCIbrXTFq4OF/9Q= charm.land/x/vcr v0.1.1/go.mod h1:eByq2gqzWvcct/8XE2XO5KznoWEBiXH56+y2gphbltM= cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE= @@ -98,8 +98,8 @@ github.com/charmbracelet/glamour/v2 v2.0.0-20251106195642-800eb8175930 h1:+47Z2j github.com/charmbracelet/glamour/v2 v2.0.0-20251106195642-800eb8175930/go.mod h1:izs11tnkYaT3DTEH2E0V/lCb18VGZ7k9HLYEGuvgXGA= github.com/charmbracelet/log/v2 v2.0.0-20251106192421-eb64aaa963a0 h1:lxHzxsHd4P7o7+5D5OcEItYkQ1xY3ovNg8Dc5ftd3rI= github.com/charmbracelet/log/v2 v2.0.0-20251106192421-eb64aaa963a0/go.mod h1:Q7oMtlboDPnnrYiJDXNwdWmJblOmuOnycPKczlVju6I= -github.com/charmbracelet/ultraviolet v0.0.0-20251202162030-ecc8c1ae4b2b h1:jY1J0PcfetoB1uJ+w8rd86gUFSpKpJJI35gnfpKF5hg= -github.com/charmbracelet/ultraviolet v0.0.0-20251202162030-ecc8c1ae4b2b/go.mod h1:Y6kE2GzHfkyQQVCSL9r2hwokSrIlHGzZG+71+wDYSZI= +github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318 h1:OqDqxQZliC7C8adA7KjelW3OjtAxREfeHkNcd66wpeI= +github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318/go.mod h1:Y6kE2GzHfkyQQVCSL9r2hwokSrIlHGzZG+71+wDYSZI= github.com/charmbracelet/x/ansi v0.11.2 h1:XAG3FSjiVtFvgEgGrNBkCNNYrsucAt8c6bfxHyROLLs= github.com/charmbracelet/x/ansi v0.11.2/go.mod h1:9tY2bzX5SiJCU0iWyskjBeI2BRQfvPqI+J760Mjf+Rg= github.com/charmbracelet/x/exp/charmtone v0.0.0-20250708181618-a60a724ba6c3 h1:1xwHZg6eMZ9Wv5TE1UGub6ARubyOd1Lo5kPUI/6VL50= diff --git a/internal/tui/page/chat/chat.go b/internal/tui/page/chat/chat.go index a5cb757d7d4403b8b99c82f5941065037bf86c71..a88ebc94506a01396d0caf68efc316fa976c46da 100644 --- a/internal/tui/page/chat/chat.go +++ b/internal/tui/page/chat/chat.go @@ -525,7 +525,7 @@ func (p *chatPage) View() string { ) layers = append(layers, lipgloss.NewLayer(details).X(1).Y(1)) } - canvas := lipgloss.NewCanvas( + canvas := lipgloss.NewCompositor( layers..., ) return canvas.Render() diff --git a/internal/tui/tui.go b/internal/tui/tui.go index 45f0ae5ec410e85b3d30d620b4db5c499cff09c3..923bf74fec63801b3f218dc49de8edab8b1fa82f 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -36,6 +36,7 @@ import ( "github.com/charmbracelet/crush/internal/tui/page/chat" "github.com/charmbracelet/crush/internal/tui/styles" "github.com/charmbracelet/crush/internal/tui/util" + uv "github.com/charmbracelet/ultraviolet" "golang.org/x/mod/semver" "golang.org/x/text/cases" "golang.org/x/text/language" @@ -87,6 +88,10 @@ type appModel struct { // QueryVersion instructs the TUI to query for the terminal version when it // starts. QueryVersion bool + + // store a reusable canvas for rendering and lazy resizing + canvas *uv.ScreenBuffer + resizeTime time.Time } // Init initializes the application model and returns initial commands. @@ -110,6 +115,8 @@ func (a appModel) Init() tea.Cmd { return tea.Batch(cmds...) } +type lazyWindowSizeMsg struct{ width, height int } + // Update handles incoming messages and updates the application state. func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmds []tea.Cmd @@ -153,9 +160,21 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } return a, tea.Batch(cmds...) case tea.WindowSizeMsg: + const debounceDuration = 50 * time.Millisecond a.wWidth, a.wHeight = msg.Width, msg.Height + // Lazily update all components on resize after 150ms + // to avoid excessive updates during window resizing. + a.resizeTime = time.Now() + return a, tea.Tick(debounceDuration, func(t time.Time) tea.Msg { + if time.Since(a.resizeTime) >= debounceDuration { + return lazyWindowSizeMsg{msg.Width, msg.Height} + } + return nil + }) + + case lazyWindowSizeMsg: a.completions.Update(msg) - return a, a.handleWindowResize(msg.Width, msg.Height) + return a, a.handleWindowResize(msg.width, msg.height) case pubsub.Event[mcp.Event]: switch msg.Payload.Type { @@ -426,6 +445,9 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (a *appModel) handleWindowResize(width, height int) tea.Cmd { var cmds []tea.Cmd + // Resize canvas + a.canvas.Resize(width, height) + // TODO: clean up these magic numbers. if a.showingFullHelp { height -= 5 @@ -593,22 +615,17 @@ func (a *appModel) View() tea.View { view.MouseMode = tea.MouseModeCellMotion view.BackgroundColor = t.BgBase if a.wWidth < 25 || a.wHeight < 15 { - view.SetContent( - lipgloss.NewCanvas( - lipgloss.NewLayer( - t.S().Base.Width(a.wWidth).Height(a.wHeight). - Align(lipgloss.Center, lipgloss.Center). - Render( - t.S().Base. - Padding(1, 4). - Foreground(t.White). - BorderStyle(lipgloss.RoundedBorder()). - BorderForeground(t.Primary). - Render("Window too small!"), - ), - ), - ).Render(), - ) + content := t.S().Base.Width(a.wWidth).Height(a.wHeight). + Align(lipgloss.Center, lipgloss.Center). + Render( + t.S().Base. + Padding(1, 4). + Foreground(t.White). + BorderStyle(lipgloss.RoundedBorder()). + BorderForeground(t.Primary). + Render("Window too small!"), + ) + view.SetContent(content) return view } @@ -659,11 +676,11 @@ func (a *appModel) View() tea.View { ) } - canvas := lipgloss.NewCanvas( - layers..., - ) + a.canvas.Clear() + comp := lipgloss.NewCompositor(layers...) + comp.Draw(a.canvas, a.canvas.Bounds()) - view.Content = canvas.Render() + view.Content = a.canvas.Render() view.Cursor = cursor if a.sendProgressBar && a.app != nil && a.app.AgentCoordinator != nil && a.app.AgentCoordinator.IsBusy() { @@ -701,6 +718,8 @@ func New(app *app.App) *appModel { keyMap := DefaultKeyMap() keyMap.pageBindings = chatPage.Bindings() + canvas := uv.NewScreenBuffer(0, 0) + model := &appModel{ currentPage: chat.ChatPageID, app: app, @@ -714,6 +733,8 @@ func New(app *app.App) *appModel { dialog: dialogs.NewDialogCmp(), completions: completions.New(), + + canvas: &canvas, } return model