chore(vendor): import pi 1.3.0

Imported as a linear vendor commit so forge rebases do not replay the upstream root tree.

git-subtree-dir: third_party/pi

git-subtree-split: 71023aec1546d6d677a65669c5665c0927ce0dd0
This commit is contained in:
alexion committed 2026-10-15 17:58:48 -04:00
1 parent 719370c8f7
commit 6081e3e325
1743 files changed
+205459 -131704

No files matched your search

+206 -14
View File
@@ -20,7 +20,7 @@ import {
resetCapabilitiesCache,
setCapabilities,
} from "../src/terminal-image.ts";
import type { TuiMouseEvent } from "../src/tui.ts";
import { renderFakeCursor, type TuiMouseEvent } from "../src/tui.ts";
import { TuiAltScreen } from "../src/tui-alt-screen.ts";
import { stripTerminalSequences, visibleWidth } from "../src/utils.ts";
import { VirtualTerminal } from "./virtual-terminal.ts";
@@ -138,7 +138,51 @@ describe("TuiAltScreen", () => {
tui.stop();
});
it("leaves the scrollbar clickable when the jump-to-end indicator spans the transcript", async () => {
it("keeps the jump-to-end indicator centered as the auto scrollbar hides and reappears", async () => {
// Regression test for #9136: auto scrollbar visibility must not move the indicator.
const terminal = new VirtualTerminal(80, 6);
const label = " ↓ Jump to latest message · End ";
const tui = new TuiAltScreen(terminal, undefined, undefined, {
scrollToEndIndicator: () => label,
});
const transcript = new ScrollView(
new Text(Array.from({ length: 20 }, (_, index) => `line ${index + 1}`).join("\n"), 0, 0),
{ follow: "end", primary: true, scrollbar: "auto", scrollbarHideDelayMs: 0 },
);
tui.setLayoutRoot(transcript);
tui.start();
try {
await terminal.waitForRender();
// Scrolling over the track keeps the scrollbar visible until the pointer leaves.
terminal.sendInput("\x1b[<64;80;1M");
await terminal.waitForRender();
assert.strictEqual(transcript.isScrollbarVisible, true);
assert.strictEqual(transcript.isFollowingEnd, false);
const scrollTop = transcript.scrollTop;
const visibleColumn = terminal.getViewport()[5].indexOf(label);
// Leaving the track lets the auto-hide timer expire without changing the content.
terminal.sendInput("\x1b[<35;79;1M");
await terminal.waitForRender();
assert.strictEqual(transcript.isScrollbarVisible, false);
assert.strictEqual(transcript.scrollTop, scrollTop);
const hiddenColumn = terminal.getViewport()[5].indexOf(label);
terminal.sendInput("\x1b[<35;80;1M");
await terminal.waitForRender();
assert.strictEqual(transcript.isScrollbarVisible, true);
assert.strictEqual(transcript.scrollTop, scrollTop);
const revealedColumn = terminal.getViewport()[5].indexOf(label);
assert.deepStrictEqual([visibleColumn, hiddenColumn, revealedColumn], [24, 24, 24]);
} finally {
tui.stop();
}
});
it("leaves the scrollbar visible and clickable when the jump-to-end indicator spans the transcript", async () => {
// Regression coverage for #9136: centering must not paint or capture clicks over the scrollbar.
const terminal = new VirtualTerminal(30, 6);
const tui = new TuiAltScreen(terminal, undefined, undefined, {
scrollToEndIndicator: () => "↓".repeat(30),
@@ -161,6 +205,7 @@ describe("TuiAltScreen", () => {
assert.strictEqual(transcript.isFollowingEnd, false);
// The indicator must not intercept a press on the scrollbar's last column.
assert.strictEqual(terminal.getViewport()[3], `${"↓".repeat(29)}┃`);
terminal.sendInput("\x1b[<0;30;4M");
terminal.sendInput("\x1b[<0;30;4m");
await terminal.waitForRender();
@@ -459,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 });
@@ -506,14 +576,14 @@ describe("TuiAltScreen", () => {
["line 5", "line 6", "line 7", "line 8", "line 9", "line 10", "line 11", "line 12"],
);
terminal.sendInput("\x1bOH");
terminal.sendInput("\x1b[7^");
await terminal.waitForRender();
assert.deepStrictEqual(
terminal.getViewport().map((line) => line.trimEnd()),
["line 1", "line 2", "line 3", "line 4", "line 5", "line 6", "line 7", "line 8"],
);
terminal.sendInput("\x1bOF");
terminal.sendInput("\x1b[8^");
await terminal.waitForRender();
assert.deepStrictEqual(
terminal.getViewport().map((line) => line.trimEnd()),
@@ -828,7 +898,7 @@ describe("TuiAltScreen", () => {
}
});
it("routes Ctrl-modified viewport navigation to the focused component", async () => {
it("routes Home and End to the focused component and Ctrl+Home/End to the transcript", async () => {
const terminal = new VirtualTerminal(20, 6);
const tui = new TuiAltScreen(terminal);
const transcript = new ScrollView(
@@ -852,22 +922,34 @@ describe("TuiAltScreen", () => {
tui.start();
await terminal.waitForRender();
terminal.sendInput("\x1bOH");
const bottom = transcript.scrollTop;
assert.ok(bottom > 0);
// #10314: unmodified Home/End belong to the editor in every UI mode.
const editorKeys = ["\x1bOH", "\x1b[F", "\x1b[57423u", "\x1b[5;5~", "\x1b[6;5~"];
for (const input of editorKeys) terminal.sendInput(input);
await terminal.waitForRender();
assert.strictEqual(transcript.scrollTop, bottom);
assert.deepStrictEqual(editorInputs, editorKeys);
terminal.sendInput("\x1b[1;5H");
await terminal.waitForRender();
assert.strictEqual(transcript.scrollTop, 0);
assert.deepStrictEqual(editorInputs, []);
const modifiedInputs = ["\x1b[1;5H", "\x1b[1;5F", "\x1b[5;5~", "\x1b[6;5~", "\x1b[57423;5u"];
for (const input of modifiedInputs) terminal.sendInput(input);
terminal.sendInput("\x1b[1;5F");
await terminal.waitForRender();
assert.strictEqual(transcript.scrollTop, bottom);
assert.strictEqual(transcript.isFollowingEnd, true);
terminal.sendInput("\x1b[57423;5u");
terminal.sendInput("\x1b[57423;5:3u");
await terminal.waitForRender();
assert.strictEqual(transcript.scrollTop, 0);
assert.deepStrictEqual(editorInputs, modifiedInputs);
terminal.sendInput("\x1b[6~");
await terminal.waitForRender();
assert.strictEqual(transcript.scrollTop, 1);
assert.deepStrictEqual(editorInputs, modifiedInputs);
assert.deepStrictEqual(editorInputs, editorKeys);
tui.stop();
});
@@ -992,6 +1074,47 @@ describe("TuiAltScreen", () => {
tui.stop();
});
it("redraws WezTerm Kitty images after writes to covered rows", async () => {
// Regression test for #10319: a scrollbar update below an unchanged image anchor erased its cells.
const weztermPane = process.env.WEZTERM_PANE;
let tui: TuiAltScreen | undefined;
process.env.WEZTERM_PANE = "1";
setCapabilities({ images: "kitty", trueColor: true, hyperlinks: true });
try {
const terminal = new RecordingTerminal(20, 4);
const imageId = 10319;
const imageLine = encodeKitty("AAAA", { columns: 2, rows: 3, imageId, moveCursor: false });
registerKittyImageMetadata({ imageId, columns: 2, rows: 3, widthPx: 100, heightPx: 100 });
let coveredLine = "";
tui = new TuiAltScreen(terminal);
tui.setLayoutRoot({
render: () => [imageLine, coveredLine, "", "after"],
invalidate: () => {},
});
tui.start();
await terminal.waitForRender();
const eventCount = terminal.events.length;
coveredLine = "changed";
tui.requestRender();
await terminal.waitForRender();
const redrawWrites = terminal.events
.slice(eventCount)
.filter((event): event is { type: "write"; data: string } => event.type === "write")
.map((event) => event.data)
.join("");
const placementIndex = redrawWrites.indexOf("\x1b_Ga=p,q=2");
assert.ok(redrawWrites.includes("\x1b_Ga=d,d=a,q=2\x1b\\"));
assert.ok(placementIndex > redrawWrites.indexOf("changed"));
assert.ok(!redrawWrites.includes("\x1b_Ga=T"));
} finally {
tui?.stop();
resetCapabilitiesCache();
if (weztermPane === undefined) delete process.env.WEZTERM_PANE;
else process.env.WEZTERM_PANE = weztermPane;
}
});
it("reuses moved Kitty images without dropping HStack siblings", async () => {
setCapabilities({ images: "kitty", trueColor: true, hyperlinks: true });
try {
@@ -1239,6 +1362,26 @@ describe("TuiAltScreen", () => {
tui.stop();
});
it("keeps the selection highlight after a fake cursor cell", async () => {
const terminal = new RecordingTerminal(20, 4);
const tui = new TuiAltScreen(terminal);
tui.addChild(new Text(`> ${renderFakeCursor("a")}bcd`, 0, 0));
tui.start();
await terminal.waitForRender();
terminal.sendInput("\x1b[<0;1;1M");
terminal.sendInput("\x1b[<32;6;1M");
terminal.sendInput("\x1b[<3;6;1m");
await terminal.waitForRender();
assert.ok(
terminal.events.some((event) => event.type === "write" && event.data.includes("\x1b[27m\x1b[7mbcd")),
"selection inverse must be reapplied after the fake cursor ends",
);
tui.stop();
});
it("uses an injected copySelection handler instead of OSC 52 and reports success", async () => {
const terminal = new RecordingTerminal(20, 4);
const copied: string[] = [];
@@ -1351,6 +1494,36 @@ describe("TuiAltScreen", () => {
tui.stop();
});
it("flashes a specific error returned by the injected copySelection handler", async () => {
// Regression test for #9618.
const terminal = new RecordingTerminal(80, 4);
const tui = new TuiAltScreen(terminal, undefined, undefined, {
copyOnSelect: false,
copySelection: async () => "Clipboard unavailable: install wl-clipboard",
});
let flashDuration: number | undefined;
const flash = tui.flash.bind(tui);
tui.flash = (message, durationMs) => {
flashDuration = durationMs;
flash(message, durationMs);
};
tui.addChild(new Text("alpha\nbeta\ngamma\ndelta", 0, 0));
tui.start();
await terminal.waitForRender();
terminal.sendInput("\x1b[<0;1;1M");
terminal.sendInput("\x1b[<32;4;2M");
terminal.sendInput("\x1b[<0;4;2m");
await terminal.waitForRender();
assert.strictEqual(await tui.copyActiveSelectionToClipboard(), false);
await terminal.waitForRender();
assert.ok(terminal.getViewport().some((line) => line.includes("Clipboard unavailable: install wl-clipboard")));
assert.ok(terminal.getViewport().every((line) => !line.includes("Copy failed")));
assert.strictEqual(flashDuration, 5000);
tui.stop();
});
it("does not append whitespace to double-click word highlighting", async () => {
const terminal = new RecordingTerminal(20, 1);
const tui = new TuiAltScreen(terminal);
@@ -1689,6 +1862,26 @@ describe("TuiAltScreen", () => {
tui.stop();
});
// #10769
it("scrolls the transcript on wheel input a focused overlay does not handle", async () => {
const terminal = new VirtualTerminal(20, 6);
const tui = new TuiAltScreen(terminal);
tui.addChild(new Text(Array.from({ length: 12 }, (_, index) => `line ${index + 1}`).join("\n"), 0, 0));
const overlay = new InputOverlay();
tui.start();
await terminal.waitForRender();
const topBefore = tui.viewportTop;
tui.showOverlay(overlay, { anchor: "top-left", width: 10 });
await terminal.waitForRender();
assert.strictEqual(overlay.focused, true);
terminal.sendInput("\x1b[<64;3;1M");
await terminal.waitForRender();
assert.ok(tui.viewportTop < topBefore);
assert.deepStrictEqual(overlay.inputs, []);
tui.stop();
});
it("focuses and captures drag gestures for mouse-aware components", async () => {
const terminal = new VirtualTerminal(20, 2);
const tui = new TuiAltScreen(terminal);
@@ -1828,7 +2021,7 @@ describe("TuiAltScreen", () => {
}
});
it("gives wheel and viewport keys to a focused overlay", async () => {
it("gives viewport keys to a focused overlay", async () => {
const terminal = new VirtualTerminal(20, 6);
const tui = new TuiAltScreen(terminal);
tui.addChild(new Text(Array.from({ length: 12 }, (_, index) => `line ${index + 1}`).join("\n"), 0, 0));
@@ -1840,8 +2033,7 @@ describe("TuiAltScreen", () => {
await terminal.waitForRender();
assert.strictEqual(overlay.focused, true);
const wheel = "\x1b[<64;10;3M";
const keys = ["\x1b[5~", "\x1b[6~", "\x1bOH", "\x1bOF", wheel];
const keys = ["\x1b[5~", "\x1b[6~", "\x1bOH", "\x1bOF"];
for (const key of keys) terminal.sendInput(key);
await terminal.waitForRender();