fix(tui): reduce mouse tracking in terminal multiplexers

This commit is contained in:
Armin Ronacher 2026-08-06 18:20:48 +02:00
parent 9b17fd5bb3
commit fc3554e16d
3 changed files with 62 additions and 4 deletions

View file

@ -5,6 +5,7 @@
### Fixed
- Fixed LaTeX relation, multiplication, and named-operator spacing, and correctly composed matrices with stacked fractions, operator limits, and adjacent matrices.
- Reduced fullscreen mouse event volume under tmux, Zellij, and GNU Screen by using button-motion tracking instead of all-motion tracking.
## [0.84.0] - 2026-08-06

View file

@ -44,7 +44,8 @@ const ENTER_ALT_SCREEN = "\x1b[?1049h";
const EXIT_ALT_SCREEN = "\x1b[?1049l";
const DISABLE_AUTOWRAP = "\x1b[?7l";
const ENABLE_AUTOWRAP = "\x1b[?7h";
const ENABLE_MOUSE = "\x1b[?1000h\x1b[?1002h\x1b[?1003h\x1b[?1004h\x1b[?1006h";
const ENABLE_BUTTON_MOTION_MOUSE = "\x1b[?1000h\x1b[?1002h\x1b[?1004h\x1b[?1006h";
const ENABLE_ALL_MOTION_MOUSE = "\x1b[?1000h\x1b[?1002h\x1b[?1003h\x1b[?1004h\x1b[?1006h";
const DISABLE_MOUSE = "\x1b[?1006l\x1b[?1004l\x1b[?1003l\x1b[?1002l\x1b[?1000l";
const FOCUS_IN = "\x1b[I";
const FOCUS_OUT = "\x1b[O";
@ -201,8 +202,19 @@ export class TuiAltScreen extends TuiBase implements ViewportTUI {
this.pressedUrl = undefined;
this.selectionDragged = false;
this.resetRenderState();
const term = process.env.TERM?.toLowerCase() ?? "";
// Multiplexers can lag when every pointer movement is forwarded. Button-motion
// tracking preserves clicks, wheel events, selections, and scrollbar dragging.
const mouseSequence =
process.env.TMUX !== undefined ||
process.env.ZELLIJ !== undefined ||
process.env.STY !== undefined ||
term.startsWith("tmux") ||
term.startsWith("screen")
? ENABLE_BUTTON_MOTION_MOUSE
: ENABLE_ALL_MOTION_MOUSE;
this.terminal.write(
`${ENTER_ALT_SCREEN}${DISABLE_AUTOWRAP}${this.mouseEnabled ? ENABLE_MOUSE : ""}\x1b[2J\x1b[H\x1b[?25l`,
`${ENTER_ALT_SCREEN}${DISABLE_AUTOWRAP}${this.mouseEnabled ? mouseSequence : ""}\x1b[2J\x1b[H\x1b[?25l`,
);
}

View file

@ -161,6 +161,53 @@ describe("TuiAltScreen", () => {
tui.stop();
});
it("uses button-motion tracking inside terminal multiplexers", () => {
const environmentKeys = ["TMUX", "ZELLIJ", "STY", "TERM"] as const;
const previousEnvironment = new Map(environmentKeys.map((key) => [key, process.env[key]]));
try {
for (const key of environmentKeys) delete process.env[key];
process.env.TERM = "xterm-256color";
const directTerminal = new RecordingTerminal();
const directTui = new TuiAltScreen(directTerminal);
directTui.start();
const directWrites = directTerminal.events
.filter((event): event is { type: "write"; data: string } => event.type === "write")
.map((event) => event.data)
.join("");
assert.ok(directWrites.includes("\x1b[?1003h"));
directTui.stop();
const multiplexers = [
{ name: "tmux environment", environment: { TMUX: "/tmp/tmux/default,1,0" } },
{ name: "tmux TERM", environment: { TERM: "tmux-256color" } },
{ name: "Zellij environment", environment: { ZELLIJ: "0" } },
{ name: "Screen environment", environment: { STY: "123.session" } },
{ name: "Screen TERM", environment: { TERM: "screen-256color" } },
];
for (const { name, environment } of multiplexers) {
for (const key of environmentKeys) delete process.env[key];
for (const [key, value] of Object.entries(environment)) process.env[key] = value;
const terminal = new RecordingTerminal();
const tui = new TuiAltScreen(terminal);
tui.start();
const writes = terminal.events
.filter((event): event is { type: "write"; data: string } => event.type === "write")
.map((event) => event.data)
.join("");
assert.ok(writes.includes("\x1b[?1002h"), `${name} should enable button-motion tracking`);
assert.ok(!writes.includes("\x1b[?1003h"), `${name} should not enable all-motion tracking`);
assert.ok(writes.includes("\x1b[?1006h"), `${name} should enable SGR mouse encoding`);
tui.stop();
}
} finally {
for (const key of environmentKeys) {
const value = previousEnvironment.get(key);
if (value === undefined) delete process.env[key];
else process.env[key] = value;
}
}
});
it("drags a visible scrollbar thumb and keeps it visible until release", async () => {
const terminal = new RecordingTerminal(10, 5);
const tui = new TuiAltScreen(terminal);
@ -214,9 +261,7 @@ describe("TuiAltScreen", () => {
assert.strictEqual(scrollView.isScrollbarVisible, false);
assert.ok(terminal.events.every((event) => event.type !== "write" || !event.data.includes("\x1b]52;c;")));
assert.ok(terminal.events.some((event) => event.type === "write" && event.data.includes("\x1b[?1003h")));
tui.stop();
assert.ok(terminal.events.some((event) => event.type === "write" && event.data.includes("\x1b[?1003l")));
});
it("keeps the scrollbar column selectable while the thumb is hidden", async () => {