@@ -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
@@ -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=
@@ -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