Skip to content

Commit fd8c52b

Browse files
committed
Stop mouse wheel from leaking escape characters into form fields
Scrolling the mouse wheel while a title or description field was focused could insert stray characters (fragments of SGR mouse escape sequences) into the field. The submit TUI ran the Bubble Tea program with WithMouseAllMotion (mode 1003), which reports an event on every pointer move. During a wheel scroll that floods the input stream, and under that volume Bubble Tea can split a mouse escape sequence across input reads; the leftover bytes of a partially-parsed sequence are then emitted as key runes and inserted into the focused text input. Switch to WithMouseCellMotion (mode 1002), which reports clicks, drag, and wheel but not idle pointer motion. That removes the input flood, so wheel sequences arrive intact and are parsed as mouse messages (consumed by handleMouse) instead of leaking as text. The TUI never used idle-hover state for rendering, so cell-motion loses nothing. Add a regression test asserting that wheel events over either panel never modify the focused title or description field.
1 parent a24bf4f commit fd8c52b

2 files changed

Lines changed: 47 additions & 1 deletion

File tree

cmd/submit.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,12 @@ func collectPRDrafts(cfg *config.Config, client github.ClientOps, s *stack.Stack
289289
Version: Version,
290290
})
291291

292-
p := tea.NewProgram(model, tea.WithAltScreen(), tea.WithMouseAllMotion())
292+
// Use cell-motion mouse mode (clicks, drag, and wheel) rather than all-motion.
293+
// All-motion (mode 1003) reports an event on every pointer move, flooding the
294+
// input; under that volume bubbletea can split an SGR mouse sequence across
295+
// reads, leaking its bytes as text into a focused title/description field
296+
// while scrolling. We don't use idle-hover, so cell-motion loses nothing.
297+
p := tea.NewProgram(model, tea.WithAltScreen(), tea.WithMouseCellMotion())
293298
final, err := p.Run()
294299
if err != nil {
295300
return nil, false, fmt.Errorf("running submit TUI: %w", err)

internal/tui/submitview/screen_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,47 @@ func TestMouse_WheelScrollsDescriptionViewport(t *testing.T) {
605605
assert.False(t, m.descScrollPinned, "typing returns to the cursor-following view")
606606
}
607607

608+
// TestMouse_WheelDoesNotEnterFieldText guards against the regression where
609+
// scrolling the mouse wheel leaked escape-sequence bytes as text into the
610+
// focused title/description field. handleMouse must consume every wheel event
611+
// (returning without forwarding it to the text inputs), so the field contents
612+
// never change regardless of which panel the pointer is over.
613+
func TestMouse_WheelDoesNotEnterFieldText(t *testing.T) {
614+
m := testModel(t, newNodes())
615+
require.Equal(t, fieldTitle, m.focusedField)
616+
617+
leftW, _ := m.panelWidths()
618+
leftX := leftW / 2 // over the left timeline
619+
rightX := leftW + 5 // over the right editor
620+
titleLine, _, _, _, _ := m.rightZones()
621+
y := m.panelTopRow() + titleLine
622+
623+
wheel := func(m Model) Model {
624+
t.Helper()
625+
for _, x := range []int{leftX, rightX} {
626+
for _, b := range []tea.MouseButton{tea.MouseButtonWheelUp, tea.MouseButtonWheelDown} {
627+
u, _ := m.Update(tea.MouseMsg{Action: tea.MouseActionPress, Button: b, X: x, Y: y})
628+
m = u.(Model)
629+
}
630+
}
631+
return m
632+
}
633+
634+
// Title focused: wheeling over either panel must not alter the title.
635+
titleBefore := m.nodes[m.cursor].Title
636+
m = wheel(m)
637+
assert.Equal(t, titleBefore, m.nodes[m.cursor].Title, "wheel must not modify the focused title")
638+
assert.Equal(t, titleBefore, m.titleInput.Value(), "wheel must not modify the title input")
639+
640+
// Description focused: same guarantee.
641+
m = sendKey(t, m, tea.KeyMsg{Type: tea.KeyTab})
642+
require.Equal(t, fieldDescription, m.focusedField)
643+
descBefore := m.nodes[m.cursor].Description
644+
m = wheel(m)
645+
assert.Equal(t, descBefore, m.nodes[m.cursor].Description, "wheel must not modify the focused description")
646+
assert.Equal(t, descBefore, m.descArea.Value(), "wheel must not modify the description input")
647+
}
648+
608649
func TestMouse_WheelScrollBoundedByContent(t *testing.T) {
609650
// The over-scroll bug only appears with a real color profile (the textarea
610651
// pads blank rows with styled spaces), so force one here.

0 commit comments

Comments
 (0)