interactive: speed up mouse scrolling in vscode

This commit is contained in:
patriceckhart 2026-05-04 10:08:11 +02:00
parent 67c7dc91ea
commit 2eab1339aa
4 changed files with 88 additions and 6 deletions

View file

@ -336,9 +336,16 @@ func (i *Interactive) Run(ctx context.Context) error {
}
}()
_, _ = term.Write([]byte(tui.SeqBracketedPasteOn))
mouseSeqOn := ""
mouseSeqOff := ""
if isVSCodeTerminal() {
mouseSeqOn = tui.SeqMouseOn
mouseSeqOff = tui.SeqMouseOff
}
_, _ = term.Write([]byte(tui.SeqBracketedPasteOn + mouseSeqOn))
_, _ = term.Write([]byte(tui.SeqAltScreenOn))
defer term.Write([]byte(tui.SeqAltScreenOff + tui.SeqBracketedPasteOff + tui.SeqShowCursor))
defer term.Write([]byte(tui.SeqAltScreenOff + mouseSeqOff + tui.SeqBracketedPasteOff + tui.SeqShowCursor))
// Streaming pacer: drains buffered text deltas at a steady rate
// so typewriter feel is identical across providers regardless of
@ -551,6 +558,21 @@ func (i *Interactive) invalidate() {
}
}
func isVSCodeTerminal() bool {
return strings.EqualFold(os.Getenv("TERM_PROGRAM"), "vscode")
}
func mouseWheelScrollRows() int {
// VS Code's integrated terminal emits relatively small wheel
// steps compared with Ghostty's native scrolling. Mouse-wheel
// events get a bigger delta than keyboard arrows so trackpads and
// wheel mice feel responsive without changing Up/Down behaviour.
if isVSCodeTerminal() {
return 12
}
return 6
}
// lastCols returns the current terminal width in columns.
func (i *Interactive) lastCols() int {
cols, _ := i.cfg.Terminal.Size()
@ -1537,6 +1559,14 @@ func (i *Interactive) handleKey(ctx context.Context, k tui.Key) (done bool) {
case tui.KeyPageDown:
i.scrollBy(-i.chatPage())
return false
case tui.KeyMouseWheelUp:
i.scrollBy(+mouseWheelScrollRows())
return false
case tui.KeyMouseWheelDown:
if i.scrollOffset > 0 {
i.scrollBy(-mouseWheelScrollRows())
}
return false
case tui.KeyUp:
// Always use up/down for chat scrolling, even when the editor
// contains text. This makes keyboard scrolling consistent with

View file

@ -42,6 +42,8 @@ const (
KeyCtrlW
KeyCtrlO
KeyPaste
KeyMouseWheelUp
KeyMouseWheelDown
KeyUnknown
)
@ -222,6 +224,22 @@ func (r *Reader) readCSI() (Key, error) {
}
func (r *Reader) dispatchCSI(params string, final byte) Key {
// SGR mouse mode: CSI < button ; x ; y M/m. Wheel events use
// button codes 64 (up) and 65 (down). We ignore coordinates for
// now; the chat view only needs scroll direction.
if strings.HasPrefix(params, "<") && (final == 'M' || final == 'm') {
parts := strings.Split(strings.TrimPrefix(params, "<"), ";")
if len(parts) >= 1 {
switch parts[0] {
case "64":
return Key{Kind: KeyMouseWheelUp}
case "65":
return Key{Kind: KeyMouseWheelDown}
}
}
return Key{Kind: KeyUnknown}
}
// Modified arrow keys come in as CSI 1;<mod><final>. Modifier values
// we care about: 2=Shift, 3=Alt/Option, 5=Ctrl. We only extract Alt.
var alt bool

View file

@ -0,0 +1,28 @@
package tui
import "testing"
func TestReaderParsesSGRMouseWheel(t *testing.T) {
cases := []struct {
seq string
want KeyKind
}{
{"\x1b[<64;10;20M", KeyMouseWheelUp},
{"\x1b[<65;10;20M", KeyMouseWheelDown},
}
for _, tc := range cases {
idx := 0
r := NewReader(func() (byte, error) {
b := tc.seq[idx]
idx++
return b, nil
})
k, err := r.Read()
if err != nil {
t.Fatalf("Read(%q): %v", tc.seq, err)
}
if k.Kind != tc.want {
t.Fatalf("Read(%q) kind=%v, want %v", tc.seq, k.Kind, tc.want)
}
}
}

View file

@ -89,10 +89,16 @@ const (
SeqClearLine = "\x1b[2K"
SeqBracketedPasteOn = "\x1b[?2004h"
SeqBracketedPasteOff = "\x1b[?2004l"
SeqAltScreenOn = "\x1b[?1049h"
SeqAltScreenOff = "\x1b[?1049l"
SeqSynchronizedOn = "\x1b[?2026h"
SeqSynchronizedOff = "\x1b[?2026l"
// Basic mouse tracking + SGR extended coordinates. Used only
// when explicitly enabled by the interactive mode (currently VS
// Code terminal) so terminals with good native scrolling, like
// Ghostty, are left alone.
SeqMouseOn = "\x1b[?1000h\x1b[?1006h"
SeqMouseOff = "\x1b[?1000l\x1b[?1006l"
SeqAltScreenOn = "\x1b[?1049h"
SeqAltScreenOff = "\x1b[?1049l"
SeqSynchronizedOn = "\x1b[?2026h"
SeqSynchronizedOff = "\x1b[?2026l"
)
// MoveTo moves the cursor to 1-indexed (row, col).