From c10fe736fd9b102dc9bd35fd87de2140f3724c7a Mon Sep 17 00:00:00 2001 From: "nekomimi.pet" Date: Thu, 24 Sep 2026 21:43:18 -0400 Subject: [PATCH] fix(cli): rebuild mouse reports that bubbletea splits across reads Bubble Tea v1's reader parses input 256 bytes at a time. When a read ends inside an SGR mouse report ("ESC[<64;19;5M"), the ESC [ is taken as an Alt+[ key and the rest arrives as typed runes, so scrolling typed "[<64;19;5M" into the prompt. Trackpad bursts fill the buffer easily. v1.3.10 parses the same way. A program filter turns the Alt+[ and "= r.want { + return r, tea.Quit + } + } + return r, nil +} + +// Bubble Tea's own reader, fed full 256-byte reads that end inside a report. +func TestBubbleTeaReaderSplitsAreRepaired(t *testing.T) { + var stream []byte + for i := 0; i < 60; i++ { + stream = append(stream, "\x1b[<64;19;5M"...) + } + run := func(filter bool) (mouse, keys int) { + model := &recorder{want: 60} + opts := []tea.ProgramOption{tea.WithInput(&chunked{data: stream, size: 256}), tea.WithoutRenderer(), tea.WithoutSignalHandler()} + if filter { + opts = append(opts, tea.WithFilter(RepairSplitMouse())) + } else { + model.want = 61 // each split adds a key + } + done := make(chan struct{}) + go func() { _, _ = tea.NewProgram(model, opts...).Run(); close(done) }() + <-done + for _, msg := range model.msgs { + switch msg.(type) { + case tea.MouseMsg: + mouse++ + case tea.KeyMsg: + keys++ + } + } + return mouse, keys + } + if mouse, keys := run(false); keys == 0 { + t.Fatalf("expected the unfiltered reader to split a report (mouse %d)", mouse) + } + if mouse, keys := run(true); mouse != 60 || keys != 0 { + t.Fatalf("filtered: %d mouse events, %d keys; want 60 and 0", mouse, keys) + } +} -- 2.54.0