feat(tui,coding-agent): configurable fullscreen wheel scrolling with auto acceleration

Adds fullscreenWheelScrollLines ("auto" or 1-100). Auto keeps one line
per event in local macOS terminals and scales with wheel velocity
elsewhere, capped at six lines per event.

closes #9758
This commit is contained in:
Armin Ronacher 2026-09-29 09:57:53 +02:00
parent 8562bcf66a
commit f1927c2d56
16 changed files with 275 additions and 20 deletions

View file

@ -504,6 +504,31 @@ describe("TuiAltScreen", () => {
tui.stop();
});
// #9758: wheel line counts can change at runtime; Alt keeps its multiplier.
it("applies runtime wheel line count updates", async () => {
const terminal = new VirtualTerminal(20, 4);
const tui = new TuiAltScreen(terminal, undefined, undefined, { wheelScrollLines: 3 });
const deltas: Array<number | undefined> = [];
tui.addChild(
new MouseRegion(new Text("wheel target", 0, 0), (event) => {
if (event.type !== "wheel") return undefined;
deltas.push(event.wheelDelta);
return { handled: true };
}),
);
tui.start();
try {
await terminal.waitForRender();
terminal.sendInput("\x1b[<64;1;1M");
tui.setWheelScrollLines(2);
terminal.sendInput("\x1b[<65;1;1M");
terminal.sendInput("\x1b[<72;1;1M");
assert.deepStrictEqual(deltas, [-3, 2, -10]);
} finally {
tui.stop();
}
});
it("chains unused wheel delta to an outer scroll view", async () => {
const terminal = new VirtualTerminal(20, 4);
const tui = new TuiAltScreen(terminal, undefined, undefined, { wheelScrollLines: 3 });

View file

@ -0,0 +1,47 @@
import assert from "node:assert";
import { describe, it } from "node:test";
import { WheelScrollAccelerator } from "../src/wheel-scroll.ts";
function scroll(accelerator: WheelScrollAccelerator, times: number[], direction: -1 | 1 = 1): number[] {
return times.map((time) => accelerator.next(direction, time));
}
// #9758: fullscreen wheel scrolling was one line per notch on terminals that do not accelerate wheels.
describe("WheelScrollAccelerator", () => {
it("uses fixed line counts regardless of timing", () => {
const accelerator = new WheelScrollAccelerator(3, true);
assert.deepStrictEqual(scroll(accelerator, [0, 10, 20, 1000]), [3, 3, 3, 3]);
accelerator.setLines(0.5);
assert.strictEqual(accelerator.next(1, 2000), 1);
});
it("keeps one line per event in auto mode when the terminal already accelerates", () => {
const accelerator = new WheelScrollAccelerator("auto", false);
assert.deepStrictEqual(scroll(accelerator, [0, 10, 20, 30]), [1, 1, 1, 1]);
});
it("scales auto mode with wheel velocity", () => {
const accelerator = new WheelScrollAccelerator("auto", true);
assert.deepStrictEqual(scroll(accelerator, [0, 150, 300, 450]), [1, 1, 1, 1]);
assert.deepStrictEqual(scroll(accelerator, [1000, 1050, 1100, 1150]), [1, 2, 2, 2]);
assert.deepStrictEqual(scroll(accelerator, [2000, 2020, 2040, 2060]), [1, 5, 5, 5]);
assert.deepStrictEqual(scroll(accelerator, [3000, 3010, 3020, 3030]), [1, 6, 6, 6]);
});
it("does not accelerate bursts of events for a single notch", () => {
const accelerator = new WheelScrollAccelerator("auto", true);
assert.deepStrictEqual(scroll(accelerator, [0, 3, 6, 9]), [1, 1, 1, 1]);
});
it("resets acceleration on direction changes and pauses", () => {
const accelerator = new WheelScrollAccelerator("auto", true);
assert.deepStrictEqual(scroll(accelerator, [0, 20, 40]), [1, 5, 5]);
assert.strictEqual(accelerator.next(-1, 60), 1);
assert.deepStrictEqual(scroll(accelerator, [500, 520]), [1, 5]);
});
it("carries fractional lines between events", () => {
const accelerator = new WheelScrollAccelerator("auto", true);
assert.deepStrictEqual(scroll(accelerator, [0, 40, 80, 120, 160]), [1, 2, 3, 2, 3]);
});
});