diff --git a/extensions/btw.ts b/extensions/btw.ts index 8c64c6b..2e4ca6c 100644 --- a/extensions/btw.ts +++ b/extensions/btw.ts @@ -1085,6 +1085,9 @@ class BtwOverlayComponent extends Container implements Focusable { this.hintsText = new Text("", 1, 0); + // Enable SGR mouse reporting so wheel/touchpad events reach handleInput(). + this.tui.terminal?.write?.("\x1b[?1000h\x1b[?1006h"); + const originalHandleInput = this.input.handleInput.bind(this.input); this.input.handleInput = (data: string) => { if (keybindings.matches(data, "app.clear")) { @@ -1141,22 +1144,53 @@ class BtwOverlayComponent extends Container implements Focusable { return Math.max(18, Math.min(32, Math.floor(terminalRows * 0.78))); } + private scrollTranscript(delta: number): void { + if (delta < 0) { + this.followTranscript = false; + } + this.transcriptScrollOffset = Math.max(0, this.transcriptScrollOffset + delta); + this.tui.requestRender(); + } + + dispose(): void { + this.tui.terminal?.write?.("\x1b[?1000l\x1b[?1006l"); + } + + private getMouseScrollDelta(data: string): number | null { + const match = data.match(/^\x1b\[<(\d+);\d+;\d+[Mm]$/); + if (!match) { + return null; + } + + const button = Number(match[1]); + if ((button & 64) !== 64) { + return null; + } + + return (button & 1) === 0 ? -3 : 3; + } + handleInput(data: string): void { if (matchesBtwFocusShortcut(data)) { this.onUnfocusCallback(); return; } - if (matchesKey(data, Key.pageUp)) { - this.followTranscript = false; - this.transcriptScrollOffset = Math.max(0, this.transcriptScrollOffset - Math.max(1, this.transcriptViewportHeight - 1)); - this.tui.requestRender(); + const mouseScrollDelta = this.getMouseScrollDelta(data); + if (mouseScrollDelta !== null) { + this.scrollTranscript(mouseScrollDelta); + return; + } + + if (matchesKey(data, Key.pageUp) || matchesKey(data, Key.up)) { + const step = matchesKey(data, Key.pageUp) ? Math.max(1, this.transcriptViewportHeight - 1) : 1; + this.scrollTranscript(-step); return; } - if (matchesKey(data, Key.pageDown)) { - this.transcriptScrollOffset += Math.max(1, this.transcriptViewportHeight - 1); - this.tui.requestRender(); + if (matchesKey(data, Key.pageDown) || matchesKey(data, Key.down)) { + const step = matchesKey(data, Key.pageDown) ? Math.max(1, this.transcriptViewportHeight - 1) : 1; + this.scrollTranscript(step); return; } @@ -1263,7 +1297,7 @@ class BtwOverlayComponent extends Container implements Focusable { const status = this.getStatus() ?? "Ready. Enter submits; Escape dismisses without clearing."; this.statusTextValue = status; this.statusText.setText(this.statusTextValue); - this.hintsTextValue = "Enter submit · Alt+/ toggle focus · Escape dismiss · PgUp/PgDn scroll"; + this.hintsTextValue = "Scroll wheel ↑↓ PgUp/PgDn · Enter · Alt+/ focus · Esc"; this.hintsText.setText(this.hintsTextValue); this.tui.requestRender(); } @@ -1655,6 +1689,7 @@ export default function (pi: ExtensionAPI) { }; runtime.close = () => { overlayDraft = overlay.getDraft(); + overlay.dispose(); closeRuntime(); };