diff --git a/go.mod b/go.mod index 7c1cf2b1b5033a3e4fb404fa0a09c83f77db8367..a8927a4b2fde67a249a81feafa49d5c8fe2c395e 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/charlievieth/fastwalk v1.0.14 github.com/charmbracelet/anthropic-sdk-go v0.0.0-20251021163913-d29170d047bf github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250820203609-601216f68ee2 - github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4.0.20251011205917-3b687ffc1619 + github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.5 github.com/charmbracelet/catwalk v0.7.0 github.com/charmbracelet/fang v0.4.3 github.com/charmbracelet/glamour/v2 v2.0.0-20250811143442-a27abb32f018 @@ -74,7 +74,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.2 - github.com/charmbracelet/ultraviolet v0.0.0-20250915111650-81d4262876ef + github.com/charmbracelet/ultraviolet v0.0.0-20251017140847-d4ace4d6e731 github.com/charmbracelet/x/cellbuf v0.0.14-0.20250811133356-e0c5dbe5ea4a // indirect github.com/charmbracelet/x/exp/slice v0.0.0-20250829135019-44e44e21330d github.com/charmbracelet/x/powernap v0.0.0-20251015113943-25f979b54ad4 @@ -148,7 +148,7 @@ require ( golang.org/x/net v0.43.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/sync v0.17.0 // indirect - golang.org/x/sys v0.36.0 // indirect + golang.org/x/sys v0.37.0 // indirect golang.org/x/term v0.35.0 // indirect golang.org/x/text v0.30.0 golang.org/x/time v0.8.0 // indirect diff --git a/go.sum b/go.sum index bbb776c23ee5a38737e960af5f17764ea977aaab..1d3160e24bcaf521b52c99e300e3ba221be4d656 100644 --- a/go.sum +++ b/go.sum @@ -78,8 +78,8 @@ github.com/charmbracelet/anthropic-sdk-go v0.0.0-20251021163913-d29170d047bf h1: github.com/charmbracelet/anthropic-sdk-go v0.0.0-20251021163913-d29170d047bf/go.mod h1:8TIYxZxsuCqqeJ0lga/b91tBwrbjoHDC66Sq5t8N2R4= github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250820203609-601216f68ee2 h1:973OHYuq2Jx9deyuPwe/6lsuQrDCatOsjP8uCd02URE= github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250820203609-601216f68ee2/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw= -github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4.0.20251011205917-3b687ffc1619 h1:hjOhtqsxa+LVuCAkzhfA43wtusOaUPyQdSTg/wbRscw= -github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4.0.20251011205917-3b687ffc1619/go.mod h1:5IzIGXU1n0foRc8bRAherC8ZuQCQURPlwx3ANLq1138= +github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.5 h1:oAChAeh730gtLKK/BpaTeJHzmj3KFuEfQ7AZgf2VGHM= +github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.5/go.mod h1:SUTLq+/pGQ5qntHgt0JswfVJFfgJgWDqyvyiSLVlmbo= github.com/charmbracelet/catwalk v0.7.0 h1:qhLv56aeel5Q+2G/YFh9k5FhTqsozsn4HYViuAQ/Rio= github.com/charmbracelet/catwalk v0.7.0/go.mod h1:ReU4SdrLfe63jkEjWMdX2wlZMV3k9r11oQAmzN0m+KY= github.com/charmbracelet/colorprofile v0.3.2 h1:9J27WdztfJQVAQKX2WOlSSRB+5gaKqqITmrvb1uTIiI= @@ -92,8 +92,8 @@ github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250917201909-41ff0bf215ea github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250917201909-41ff0bf215ea/go.mod h1:ngHerf1JLJXBrDXdphn5gFrBPriCL437uwukd5c93pM= 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-20250915111650-81d4262876ef h1:VrWaUi2LXYLjfjCHowdSOEc6dQ9Ro14KY7Bw4IWd19M= -github.com/charmbracelet/ultraviolet v0.0.0-20250915111650-81d4262876ef/go.mod h1:AThRsQH1t+dfyOKIwXRoJBniYFQUkUpQq4paheHMc2o= +github.com/charmbracelet/ultraviolet v0.0.0-20251017140847-d4ace4d6e731 h1:Lr+igmzKpLPdb8yUZBP9noYWwCZP042z2nWPrJZTc+8= +github.com/charmbracelet/ultraviolet v0.0.0-20251017140847-d4ace4d6e731/go.mod h1:KfWwUa0Oe//D72YlhbOq/g40L7UiGtATrvsGI3cciG8= github.com/charmbracelet/x/ansi v0.10.2 h1:ith2ArZS0CJG30cIUfID1LXN7ZFXRCww6RUvAPA+Pzw= github.com/charmbracelet/x/ansi v0.10.2/go.mod h1:HbLdJjQH4UH4AqA2HpRWuWNluRE6zxJH/yteYEYCFa8= github.com/charmbracelet/x/cellbuf v0.0.14-0.20250811133356-e0c5dbe5ea4a h1:zYSNtEJM9jwHbJts2k+Hroj+xQwsW1yxc4Wopdv7KaI= @@ -388,8 +388,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= diff --git a/internal/cmd/root.go b/internal/cmd/root.go index d6a26d818643a05704f554223a7b7960792970c5..005f2e86f7012b265fb619580c7cc2eec2e4de03 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -83,11 +83,8 @@ crush -y // Set up the TUI. program := tea.NewProgram( tui.New(app), - tea.WithAltScreen(), tea.WithContext(cmd.Context()), - tea.WithMouseCellMotion(), // Use cell motion instead of all motion to reduce event flooding - tea.WithFilter(tui.MouseEventFilter), // Filter mouse events based on focus state - ) + tea.WithFilter(tui.MouseEventFilter)) // Filter mouse events based on focus state go app.Subscribe(program) diff --git a/internal/format/spinner.go b/internal/format/spinner.go index 69e443d0f67adadd1e3f9b9a13129850324b6184..d1d9805f4b7bd511270316c4b1f0dafdfe9401b3 100644 --- a/internal/format/spinner.go +++ b/internal/format/spinner.go @@ -23,8 +23,8 @@ type model struct { anim *anim.Anim } -func (m model) Init() tea.Cmd { return m.anim.Init() } -func (m model) View() string { return m.anim.View() } +func (m model) Init() tea.Cmd { return m.anim.Init() } +func (m model) View() tea.View { return tea.NewView(m.anim.View()) } // Update implements tea.Model. func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { diff --git a/internal/tui/components/anim/anim.go b/internal/tui/components/anim/anim.go index 05ac4da98281248d1774a10e95f4d8e2f177e048..d04176ba9e07c2ce15427e9496cf0896222ba930 100644 --- a/internal/tui/components/anim/anim.go +++ b/internal/tui/components/anim/anim.go @@ -16,6 +16,7 @@ import ( "github.com/lucasb-eyer/go-colorful" "github.com/charmbracelet/crush/internal/csync" + "github.com/charmbracelet/crush/internal/tui/util" ) const ( @@ -318,7 +319,7 @@ func (a *Anim) Init() tea.Cmd { } // Update processes animation steps (or not). -func (a *Anim) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (a *Anim) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case StepMsg: if msg.id != a.id { diff --git a/internal/tui/components/chat/chat.go b/internal/tui/components/chat/chat.go index 8688f7e24c94290c74ae4344499acff61b43ac39..aaaf683494a8dd1608d9ebae4f07dae6037def26 100644 --- a/internal/tui/components/chat/chat.go +++ b/internal/tui/components/chat/chat.go @@ -101,7 +101,7 @@ 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) { +func (m *messageListCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { var cmds []tea.Cmd if m.session.ID != "" && m.app.CoderAgent != nil { queueSize := m.app.CoderAgent.QueuedPrompts(m.session.ID) diff --git a/internal/tui/components/chat/editor/editor.go b/internal/tui/components/chat/editor/editor.go index 9ccd453d3f6f200c43012b61a7545fb3c08a4e6a..92c6bea70c3e43af1b92f03c30ba3e15af0f5e4d 100644 --- a/internal/tui/components/chat/editor/editor.go +++ b/internal/tui/components/chat/editor/editor.go @@ -172,7 +172,7 @@ func (m *editorCmp) repositionCompletions() tea.Msg { return completions.RepositionCompletionsMsg{X: x, Y: y} } -func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m *editorCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { var cmd tea.Cmd var cmds []tea.Cmd switch msg := msg.(type) { diff --git a/internal/tui/components/chat/header/header.go b/internal/tui/components/chat/header/header.go index 21861a4a2eda1340f6e01c0748f24cb713f15398..6bea86690b4ffe813799ad6e1ba01359562ed791 100644 --- a/internal/tui/components/chat/header/header.go +++ b/internal/tui/components/chat/header/header.go @@ -44,7 +44,7 @@ func (h *header) Init() tea.Cmd { return nil } -func (h *header) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (h *header) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case pubsub.Event[session.Session]: if msg.Type == pubsub.UpdatedEvent { diff --git a/internal/tui/components/chat/messages/messages.go b/internal/tui/components/chat/messages/messages.go index d931ba7e179255d6639db78ebea5e82b57af1504..eb0e9e84c3cd1342126b8de9acef6d145a15bb62 100644 --- a/internal/tui/components/chat/messages/messages.go +++ b/internal/tui/components/chat/messages/messages.go @@ -35,7 +35,7 @@ var ClearSelectionKey = key.NewBinding(key.WithKeys("esc", "alt+esc"), key.WithH // MessageCmp defines the interface for message components in the chat interface. // It combines standard UI model interfaces with message-specific functionality. type MessageCmp interface { - util.Model // Basic Bubble Tea model interface + util.Model // Basic Bubble util.Model interface layout.Sizeable // Width/height management layout.Focusable // Focus state management GetMessage() message.Message // Access to underlying message data @@ -94,7 +94,7 @@ func (m *messageCmp) Init() tea.Cmd { // Update handles incoming messages and updates the component state. // Manages animation updates for spinning messages and stops animation when appropriate. -func (m *messageCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m *messageCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case anim.StepMsg: m.spinning = m.shouldSpin() @@ -384,7 +384,7 @@ func (m *assistantSectionModel) Init() tea.Cmd { return nil } -func (m *assistantSectionModel) Update(tea.Msg) (tea.Model, tea.Cmd) { +func (m *assistantSectionModel) Update(tea.Msg) (util.Model, tea.Cmd) { return m, nil } diff --git a/internal/tui/components/chat/messages/tool.go b/internal/tui/components/chat/messages/tool.go index 7e03674f97243e7d9e569b341fe1c6f1d2450b93..1899a47ec14ab185f06f64063223a0fc5d24826f 100644 --- a/internal/tui/components/chat/messages/tool.go +++ b/internal/tui/components/chat/messages/tool.go @@ -27,7 +27,7 @@ import ( // ToolCallCmp defines the interface for tool call components in the chat interface. // It manages the display of tool execution including pending states, results, and errors. type ToolCallCmp interface { - util.Model // Basic Bubble Tea model interface + util.Model // Basic Bubble util.Model interface layout.Sizeable // Width/height management layout.Focusable // Focus state management GetToolCall() message.ToolCall // Access to tool call data @@ -147,7 +147,7 @@ func (m *toolCallCmp) Init() tea.Cmd { // Update handles incoming messages and updates the component state. // Manages animation updates for pending tool calls. -func (m *toolCallCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m *toolCallCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case anim.StepMsg: var cmds []tea.Cmd @@ -160,7 +160,7 @@ func (m *toolCallCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } if m.spinning { u, cmd := m.anim.Update(msg) - m.anim = u.(util.Model) + m.anim = u cmds = append(cmds, cmd) } return m, tea.Batch(cmds...) diff --git a/internal/tui/components/chat/sidebar/sidebar.go b/internal/tui/components/chat/sidebar/sidebar.go index b50a78c7f8697e4f4db19649a01794cfe7a23bac..28808e0a8e57df881263d2fb90d25dfe8d02b83b 100644 --- a/internal/tui/components/chat/sidebar/sidebar.go +++ b/internal/tui/components/chat/sidebar/sidebar.go @@ -88,7 +88,7 @@ func (m *sidebarCmp) Init() tea.Cmd { return nil } -func (m *sidebarCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m *sidebarCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case SessionFilesMsg: m.files = csync.NewMap[string, SessionFile]() diff --git a/internal/tui/components/chat/splash/splash.go b/internal/tui/components/chat/splash/splash.go index 187fc35e6ec47a858b99f35e135a8cef3500fbf1..e6446a981754665ba32beca48dc4a395addc5b93 100644 --- a/internal/tui/components/chat/splash/splash.go +++ b/internal/tui/components/chat/splash/splash.go @@ -135,7 +135,7 @@ func (s *splashCmp) SetSize(width int, height int) tea.Cmd { } // Update implements SplashPage. -func (s *splashCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: return s, s.SetSize(msg.Width, msg.Height) diff --git a/internal/tui/components/completions/completions.go b/internal/tui/components/completions/completions.go index 1d8a0a854197d3b7ba9e26426bde8a2679f79573..93c1b6498f418c23a17ef0738d5748e25d04a685 100644 --- a/internal/tui/components/completions/completions.go +++ b/internal/tui/components/completions/completions.go @@ -112,7 +112,7 @@ func (c *completionsCmp) Init() tea.Cmd { } // Update implements Completions. -func (c *completionsCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (c *completionsCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: c.wWidth, c.wHeight = msg.Width, msg.Height diff --git a/internal/tui/components/core/status/status.go b/internal/tui/components/core/status/status.go index b01873a22b18f87d798757bb5a6ba799ae0e7a81..effbaac9d48c8600c2b9b0e7dce94b9bbf5b429b 100644 --- a/internal/tui/components/core/status/status.go +++ b/internal/tui/components/core/status/status.go @@ -36,7 +36,7 @@ func (m *statusCmp) Init() tea.Cmd { return nil } -func (m *statusCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m *statusCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: m.width = msg.Width diff --git a/internal/tui/components/dialogs/commands/arguments.go b/internal/tui/components/dialogs/commands/arguments.go index 72677bc934864970c2cbded87b31853ad702a6ed..66ad3f7ba06ae41fa2a4d0e033906ceda5298c22 100644 --- a/internal/tui/components/dialogs/commands/arguments.go +++ b/internal/tui/components/dialogs/commands/arguments.go @@ -92,7 +92,7 @@ func (c *commandArgumentsDialogCmp) Init() tea.Cmd { } // Update implements CommandArgumentsDialog. -func (c *commandArgumentsDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (c *commandArgumentsDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: c.wWidth = msg.Width diff --git a/internal/tui/components/dialogs/commands/commands.go b/internal/tui/components/dialogs/commands/commands.go index 664158fc392a87d8a7725bfa964748f7ef4f8e67..d05ec8fea44415ab83158849319cde62f96ef329 100644 --- a/internal/tui/components/dialogs/commands/commands.go +++ b/internal/tui/components/dialogs/commands/commands.go @@ -116,7 +116,7 @@ func (c *commandDialogCmp) Init() tea.Cmd { return c.SetCommandType(c.commandType) } -func (c *commandDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (c *commandDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: c.wWidth = msg.Width diff --git a/internal/tui/components/dialogs/compact/compact.go b/internal/tui/components/dialogs/compact/compact.go index ecde402fd8dfe1f31791834cd4e4bae13ec45e00..12cea72b55b4b3ad4f11c2f756ad7961ba3c8f87 100644 --- a/internal/tui/components/dialogs/compact/compact.go +++ b/internal/tui/components/dialogs/compact/compact.go @@ -61,7 +61,7 @@ func (c *compactDialogCmp) Init() tea.Cmd { return nil } -func (c *compactDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (c *compactDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: c.wWidth = msg.Width diff --git a/internal/tui/components/dialogs/dialogs.go b/internal/tui/components/dialogs/dialogs.go index 99e14e51fdd271a9cee0c27528c7608ea28fa24e..d5ad83c160e0e618e637dabe2b5e297ff0c1cd65 100644 --- a/internal/tui/components/dialogs/dialogs.go +++ b/internal/tui/components/dialogs/dialogs.go @@ -32,7 +32,7 @@ type CloseDialogMsg struct{} // DialogCmp manages a stack of dialogs with keyboard navigation. type DialogCmp interface { - tea.Model + util.Model Dialogs() []DialogModel HasDialogs() bool @@ -62,7 +62,7 @@ func (d dialogCmp) Init() tea.Cmd { } // Update handles dialog lifecycle and forwards messages to the active dialog. -func (d dialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (d dialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: var cmds []tea.Cmd @@ -98,7 +98,11 @@ func (d dialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return d, nil } -func (d dialogCmp) handleOpen(msg OpenDialogMsg) (tea.Model, tea.Cmd) { +func (d dialogCmp) View() string { + return "" +} + +func (d dialogCmp) handleOpen(msg OpenDialogMsg) (util.Model, tea.Cmd) { if d.HasDialogs() { dialog := d.dialogs[len(d.dialogs)-1] if dialog.ID() == msg.Model.ID() { diff --git a/internal/tui/components/dialogs/filepicker/filepicker.go b/internal/tui/components/dialogs/filepicker/filepicker.go index fcec2fc8b6e3e606e555c55949049f397a30f921..85a391ce5ceba7689148fbdcd016b73c1e100f54 100644 --- a/internal/tui/components/dialogs/filepicker/filepicker.go +++ b/internal/tui/components/dialogs/filepicker/filepicker.go @@ -88,7 +88,7 @@ func (m *model) Init() tea.Cmd { return m.filePicker.Init() } -func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m *model) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: m.wWidth = msg.Width diff --git a/internal/tui/components/dialogs/models/apikey.go b/internal/tui/components/dialogs/models/apikey.go index 0490335f9ad745839a94de0460a0fc5c1b6f125c..1c4ee0c14a77e2006d2bd43e40947b6852fa1736 100644 --- a/internal/tui/components/dialogs/models/apikey.go +++ b/internal/tui/components/dialogs/models/apikey.go @@ -9,6 +9,7 @@ import ( "github.com/charmbracelet/crush/internal/config" "github.com/charmbracelet/crush/internal/home" "github.com/charmbracelet/crush/internal/tui/styles" + "github.com/charmbracelet/crush/internal/tui/util" "github.com/charmbracelet/lipgloss/v2" ) @@ -75,7 +76,7 @@ func (a *APIKeyInput) Init() tea.Cmd { return a.spinner.Tick } -func (a *APIKeyInput) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (a *APIKeyInput) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case spinner.TickMsg: if a.state == APIKeyInputStateVerifying { diff --git a/internal/tui/components/dialogs/models/models.go b/internal/tui/components/dialogs/models/models.go index 7c2863706c29180cffcfb88c385a012e39df464c..2e0b68cc3640c9ee5ed411eb10a07e9dc3bc0635 100644 --- a/internal/tui/components/dialogs/models/models.go +++ b/internal/tui/components/dialogs/models/models.go @@ -98,7 +98,7 @@ func (m *modelDialogCmp) Init() tea.Cmd { return tea.Batch(m.modelList.Init(), m.apiKeyInput.Init()) } -func (m *modelDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m *modelDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: m.wWidth = msg.Width diff --git a/internal/tui/components/dialogs/permissions/permissions.go b/internal/tui/components/dialogs/permissions/permissions.go index 9e0a6b05d7385c354f8faba3110b1c0951f9a97d..7705edd394bd91466220326c474ea2b8ef55ffc7 100644 --- a/internal/tui/components/dialogs/permissions/permissions.go +++ b/internal/tui/components/dialogs/permissions/permissions.go @@ -95,7 +95,7 @@ func (p *permissionDialogCmp) supportsDiffView() bool { return p.permission.ToolName == tools.EditToolName || p.permission.ToolName == tools.WriteToolName || p.permission.ToolName == tools.MultiEditToolName } -func (p *permissionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (p *permissionDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { var cmds []tea.Cmd switch msg := msg.(type) { diff --git a/internal/tui/components/dialogs/quit/quit.go b/internal/tui/components/dialogs/quit/quit.go index 763dc842d386a072176e1a26741d8b68c1e2993b..a8857104550886abc5f70956bf384ab2df6ec302 100644 --- a/internal/tui/components/dialogs/quit/quit.go +++ b/internal/tui/components/dialogs/quit/quit.go @@ -40,7 +40,7 @@ func (q *quitDialogCmp) Init() tea.Cmd { } // Update handles keyboard input for the quit dialog. -func (q *quitDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (q *quitDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: q.wWidth = msg.Width diff --git a/internal/tui/components/dialogs/reasoning/reasoning.go b/internal/tui/components/dialogs/reasoning/reasoning.go index ba49abd8c58a0e7eb84235e7b68f5f5193a96b1b..81f521c4bd31daa25fad5ccfb127a80ea2f20eba 100644 --- a/internal/tui/components/dialogs/reasoning/reasoning.go +++ b/internal/tui/components/dialogs/reasoning/reasoning.go @@ -172,7 +172,7 @@ func (r *reasoningDialogCmp) populateEffortOptions() tea.Cmd { return nil } -func (r *reasoningDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (r *reasoningDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: r.wWidth = msg.Width diff --git a/internal/tui/components/dialogs/sessions/sessions.go b/internal/tui/components/dialogs/sessions/sessions.go index 037eb5ebb727a24b8ab9bfda2e2c72943120e819..7f01f3ba4dacfe408fed0e8f5a2f34b39d8b2edd 100644 --- a/internal/tui/components/dialogs/sessions/sessions.go +++ b/internal/tui/components/dialogs/sessions/sessions.go @@ -81,7 +81,7 @@ func (s *sessionDialogCmp) Init() tea.Cmd { return tea.Sequence(cmds...) } -func (s *sessionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (s *sessionDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: var cmds []tea.Cmd diff --git a/internal/tui/exp/list/filterable.go b/internal/tui/exp/list/filterable.go index d5c47b01083cdc1becaed9aac4fb8a5d3e9f3b47..b93f8cc3309f66fb957c40e0d6b25419ef51d4e7 100644 --- a/internal/tui/exp/list/filterable.go +++ b/internal/tui/exp/list/filterable.go @@ -9,6 +9,7 @@ import ( tea "github.com/charmbracelet/bubbletea/v2" "github.com/charmbracelet/crush/internal/tui/components/core/layout" "github.com/charmbracelet/crush/internal/tui/styles" + "github.com/charmbracelet/crush/internal/tui/util" "github.com/charmbracelet/lipgloss/v2" "github.com/sahilm/fuzzy" ) @@ -116,7 +117,7 @@ func NewFilterableList[T FilterableItem](items []T, opts ...filterableListOption return f } -func (f *filterableList[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (f *filterableList[T]) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyPressMsg: switch { diff --git a/internal/tui/exp/list/filterable_group.go b/internal/tui/exp/list/filterable_group.go index 6e9a5dc7eaad66d32ec34baf7e41d35ab3233048..15084cce28be5190367eba861a491231139af53f 100644 --- a/internal/tui/exp/list/filterable_group.go +++ b/internal/tui/exp/list/filterable_group.go @@ -11,6 +11,7 @@ import ( tea "github.com/charmbracelet/bubbletea/v2" "github.com/charmbracelet/crush/internal/tui/components/core/layout" "github.com/charmbracelet/crush/internal/tui/styles" + "github.com/charmbracelet/crush/internal/tui/util" "github.com/charmbracelet/lipgloss/v2" "github.com/sahilm/fuzzy" ) @@ -65,7 +66,7 @@ func NewFilterableGroupedList[T FilterableItem](items []Group[T], opts ...filter return f } -func (f *filterableGroupList[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (f *filterableGroupList[T]) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyPressMsg: switch { diff --git a/internal/tui/exp/list/grouped.go b/internal/tui/exp/list/grouped.go index cb54628a70e84cb80eeb162a0d9f836f14271641..43223602dbfbeaa0ae60d0368b95a4f455228f96 100644 --- a/internal/tui/exp/list/grouped.go +++ b/internal/tui/exp/list/grouped.go @@ -58,7 +58,7 @@ func (g *groupedList[T]) Init() tea.Cmd { return g.render() } -func (l *groupedList[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (l *groupedList[T]) Update(msg tea.Msg) (util.Model, tea.Cmd) { u, cmd := l.list.Update(msg) l.list = u.(*list[Item]) return l, cmd diff --git a/internal/tui/exp/list/items.go b/internal/tui/exp/list/items.go index 9e7259dc10a61c95e970d9f1fc93b0d61d7a65a8..143908d5416be744424cc30965b8d663ca2a2c68 100644 --- a/internal/tui/exp/list/items.go +++ b/internal/tui/exp/list/items.go @@ -7,6 +7,7 @@ import ( "github.com/charmbracelet/crush/internal/tui/components/core" "github.com/charmbracelet/crush/internal/tui/components/core/layout" "github.com/charmbracelet/crush/internal/tui/styles" + "github.com/charmbracelet/crush/internal/tui/util" "github.com/charmbracelet/lipgloss/v2" "github.com/charmbracelet/x/ansi" "github.com/google/uuid" @@ -97,7 +98,7 @@ func (c *completionItemCmp[T]) Init() tea.Cmd { } // Update implements CommandItem. -func (c *completionItemCmp[T]) Update(tea.Msg) (tea.Model, tea.Cmd) { +func (c *completionItemCmp[T]) Update(tea.Msg) (util.Model, tea.Cmd) { return c, nil } @@ -348,7 +349,7 @@ func (m *itemSectionModel) Init() tea.Cmd { return nil } -func (m *itemSectionModel) Update(tea.Msg) (tea.Model, tea.Cmd) { +func (m *itemSectionModel) Update(tea.Msg) (util.Model, tea.Cmd) { return m, nil } diff --git a/internal/tui/exp/list/list.go b/internal/tui/exp/list/list.go index e18b88348959c59190f1741698f76c33f04571db..ea04b0c7d640f7801ba320d26071e21eafbbd90c 100644 --- a/internal/tui/exp/list/list.go +++ b/internal/tui/exp/list/list.go @@ -217,7 +217,7 @@ func (l *list[T]) Init() tea.Cmd { } // Update implements List. -func (l *list[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (l *list[T]) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case tea.MouseWheelMsg: if l.enableMouse { @@ -277,7 +277,7 @@ func (l *list[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return l, nil } -func (l *list[T]) handleMouseWheel(msg tea.MouseWheelMsg) (tea.Model, tea.Cmd) { +func (l *list[T]) handleMouseWheel(msg tea.MouseWheelMsg) (util.Model, tea.Cmd) { var cmd tea.Cmd switch msg.Button { case tea.MouseWheelDown: diff --git a/internal/tui/exp/list/list_test.go b/internal/tui/exp/list/list_test.go index 63cfa599e8ce1c96aad1cae67243caa2b097ee0b..4e6d8e3110d8c585b26293b7ef1f1e80e06c8b50 100644 --- a/internal/tui/exp/list/list_test.go +++ b/internal/tui/exp/list/list_test.go @@ -7,6 +7,7 @@ import ( tea "github.com/charmbracelet/bubbletea/v2" "github.com/charmbracelet/crush/internal/tui/components/core/layout" + "github.com/charmbracelet/crush/internal/tui/util" "github.com/charmbracelet/lipgloss/v2" "github.com/charmbracelet/x/exp/golden" "github.com/google/uuid" @@ -602,7 +603,7 @@ func (s *simpleItem) Init() tea.Cmd { return nil } -func (s *simpleItem) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (s *simpleItem) Update(msg tea.Msg) (util.Model, tea.Cmd) { return s, nil } @@ -644,7 +645,7 @@ func (s *selectableItem) IsFocused() bool { return s.focused } -func execCmd(m tea.Model, cmd tea.Cmd) { +func execCmd(m util.Model, cmd tea.Cmd) { for cmd != nil { msg := cmd() m, cmd = m.Update(msg) diff --git a/internal/tui/page/chat/chat.go b/internal/tui/page/chat/chat.go index 2918925068cb2f012bead47bbf44260c6255288c..1559d314d052d118019797f85eedd91a7e0f6d00 100644 --- a/internal/tui/page/chat/chat.go +++ b/internal/tui/page/chat/chat.go @@ -163,7 +163,7 @@ func (p *chatPage) Init() tea.Cmd { ) } -func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (p *chatPage) Update(msg tea.Msg) (util.Model, tea.Cmd) { var cmds []tea.Cmd switch msg := msg.(type) { case tea.KeyboardEnhancementsMsg: diff --git a/internal/tui/tui.go b/internal/tui/tui.go index 26d23f46ee62aafe07d1bb6209a4fedea929c6e1..efaf1dbb9431bc3a69fb08278b65f9b34ac281fa 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -91,8 +91,6 @@ func (a appModel) Init() tea.Cmd { cmd = a.status.Init() cmds = append(cmds, cmd) - cmds = append(cmds, tea.EnableMouseAllMotion) - return tea.Batch(cmds...) } @@ -106,9 +104,7 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.KeyboardEnhancementsMsg: for id, page := range a.pages { m, pageCmd := page.Update(msg) - if model, ok := m.(util.Model); ok { - a.pages[id] = model - } + a.pages[id] = m if pageCmd != nil { cmds = append(cmds, pageCmd) @@ -232,9 +228,7 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Forward to view. updated, itemCmd := item.Update(msg) - if model, ok := updated.(util.Model); ok { - a.pages[a.currentPage] = model - } + a.pages[a.currentPage] = updated return a, itemCmd case pubsub.Event[permission.PermissionRequest]: @@ -292,9 +286,7 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { a.isConfigured = config.HasInitialDataConfig() updated, pageCmd := item.Update(msg) - if model, ok := updated.(util.Model); ok { - a.pages[a.currentPage] = model - } + a.pages[a.currentPage] = updated cmds = append(cmds, pageCmd) return a, tea.Batch(cmds...) @@ -314,9 +306,7 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } updated, pageCmd := item.Update(msg) - if model, ok := updated.(util.Model); ok { - a.pages[a.currentPage] = model - } + a.pages[a.currentPage] = updated cmds = append(cmds, pageCmd) } @@ -336,9 +326,7 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } updated, pageCmd := item.Update(msg) - if model, ok := updated.(util.Model); ok { - a.pages[a.currentPage] = model - } + a.pages[a.currentPage] = updated cmds = append(cmds, pageCmd) } @@ -353,9 +341,7 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } updated, cmd := item.Update(msg) - if model, ok := updated.(util.Model); ok { - a.pages[a.currentPage] = model - } + a.pages[a.currentPage] = updated if a.dialog.HasDialogs() { u, dialogCmd := a.dialog.Update(msg) @@ -391,9 +377,7 @@ func (a *appModel) handleWindowResize(width, height int) tea.Cmd { // Update the current view. for p, page := range a.pages { updated, pageCmd := page.Update(tea.WindowSizeMsg{Width: width, Height: height}) - if model, ok := updated.(util.Model); ok { - a.pages[p] = model - } + a.pages[p] = updated cmds = append(cmds, pageCmd) } @@ -496,9 +480,7 @@ func (a *appModel) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd { } updated, cmd := item.Update(msg) - if model, ok := updated.(util.Model); ok { - a.pages[a.currentPage] = model - } + a.pages[a.currentPage] = updated return cmd } } @@ -602,6 +584,8 @@ func (a *appModel) View() tea.View { view.Layer = canvas view.Cursor = cursor + view.MouseMode = tea.MouseModeCellMotion + view.AltScreen = true if a.app != nil && a.app.CoderAgent != nil && a.app.CoderAgent.IsBusy() { // HACK: use a random percentage to prevent ghostty from hiding it // after a timeout. diff --git a/internal/tui/util/util.go b/internal/tui/util/util.go index eb19ad89544b281af2e836f667ac63aaa6414e01..c3ce1dbf7ad94cc89def5e6a11da94b540f7b38e 100644 --- a/internal/tui/util/util.go +++ b/internal/tui/util/util.go @@ -12,8 +12,9 @@ type Cursor interface { } type Model interface { - tea.Model - tea.ViewModel + Init() tea.Cmd + Update(tea.Msg) (Model, tea.Cmd) + View() string } func CmdHandler(msg tea.Msg) tea.Cmd {