mirror of
https://github.com/earendil-works/pi.git
synced 2026-06-18 15:54:04 +08:00
+65
-9
@@ -1309,12 +1309,56 @@ export class TUI extends Container {
|
||||
return;
|
||||
}
|
||||
|
||||
// Content shrunk below the working area and no overlays - re-render to clear empty rows
|
||||
// (overlays need the padding, so only do this when no overlays are active)
|
||||
// Configurable via setClearOnShrink() or PI_CLEAR_ON_SHRINK=0 env var
|
||||
if (this.clearOnShrink && newLines.length < this.maxLinesRendered && this.overlayStack.length === 0) {
|
||||
logRedraw(`clearOnShrink (maxLinesRendered=${this.maxLinesRendered})`);
|
||||
fullRender(true);
|
||||
const redrawViewport = (nextViewportTop: number): void => {
|
||||
let buffer = "\x1b[?2026h";
|
||||
buffer += this.deleteChangedKittyImages(prevViewportTop, prevViewportTop + height - 1);
|
||||
|
||||
let screenRow = 0;
|
||||
while (screenRow < height) {
|
||||
const lineIndex = nextViewportTop + screenRow;
|
||||
const line = newLines[lineIndex] ?? "";
|
||||
const isImage = isImageLine(line);
|
||||
const imageReservedRows = isImage
|
||||
? this.getKittyImageReservedRows(newLines, lineIndex, nextViewportTop + height - 1)
|
||||
: 1;
|
||||
|
||||
buffer += `\x1b[${screenRow + 1};1H`;
|
||||
buffer += "\x1b[2K";
|
||||
|
||||
if (isImage && imageReservedRows > 1 && screenRow + imageReservedRows <= height) {
|
||||
for (let row = 1; row < imageReservedRows; row++) {
|
||||
buffer += `\x1b[${screenRow + row + 1};1H`;
|
||||
buffer += "\x1b[2K";
|
||||
}
|
||||
buffer += `\x1b[${screenRow + 1};1H`;
|
||||
buffer += line;
|
||||
screenRow += imageReservedRows;
|
||||
continue;
|
||||
}
|
||||
|
||||
buffer += line;
|
||||
screenRow += 1;
|
||||
}
|
||||
|
||||
buffer += "\x1b[?2026l";
|
||||
this.terminal.write(buffer);
|
||||
this.cursorRow = Math.max(0, newLines.length - 1);
|
||||
this.hardwareCursorRow = nextViewportTop + height - 1;
|
||||
this.maxLinesRendered = newLines.length;
|
||||
this.previousViewportTop = nextViewportTop;
|
||||
this.positionHardwareCursor(cursorPos, newLines.length);
|
||||
this.previousLines = newLines;
|
||||
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
|
||||
this.previousWidth = width;
|
||||
this.previousHeight = height;
|
||||
};
|
||||
|
||||
const shrinkClearEnabled =
|
||||
this.clearOnShrink && newLines.length < this.maxLinesRendered && this.overlayStack.length === 0;
|
||||
const shrinkViewportTop = Math.max(0, newLines.length - height);
|
||||
if (shrinkClearEnabled && shrinkViewportTop < prevViewportTop) {
|
||||
logRedraw(`clearOnShrink viewport redraw (maxLinesRendered=${this.maxLinesRendered})`);
|
||||
redrawViewport(shrinkViewportTop);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1364,7 +1408,11 @@ export class TUI extends Container {
|
||||
const targetRow = Math.max(0, newLines.length - 1);
|
||||
if (targetRow < prevViewportTop) {
|
||||
logRedraw(`deleted lines moved viewport up (${targetRow} < ${prevViewportTop})`);
|
||||
fullRender(true);
|
||||
if (shrinkClearEnabled) {
|
||||
redrawViewport(shrinkViewportTop);
|
||||
} else {
|
||||
fullRender(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const lineDiff = computeLineDiff(targetRow);
|
||||
@@ -1375,7 +1423,11 @@ export class TUI extends Container {
|
||||
const extraLines = this.previousLines.length - newLines.length;
|
||||
if (extraLines > height) {
|
||||
logRedraw(`extraLines > height (${extraLines} > ${height})`);
|
||||
fullRender(true);
|
||||
if (shrinkClearEnabled) {
|
||||
redrawViewport(shrinkViewportTop);
|
||||
} else {
|
||||
fullRender(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const clearStartOffset = newLines.length === 0 ? 0 : 1;
|
||||
@@ -1408,7 +1460,11 @@ export class TUI extends Container {
|
||||
// If the first changed line is above the previous viewport, we need a full redraw.
|
||||
if (firstChanged < prevViewportTop) {
|
||||
logRedraw(`firstChanged < viewportTop (${firstChanged} < ${prevViewportTop})`);
|
||||
fullRender(true);
|
||||
if (shrinkClearEnabled) {
|
||||
redrawViewport(shrinkViewportTop);
|
||||
} else {
|
||||
fullRender(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -61,8 +61,12 @@ async function withEnv<T>(updates: Record<string, string | undefined>, run: () =
|
||||
}
|
||||
}
|
||||
|
||||
function getXterm(terminal: VirtualTerminal): XtermTerminalType {
|
||||
return (terminal as unknown as { xterm: XtermTerminalType }).xterm;
|
||||
}
|
||||
|
||||
function getCellItalic(terminal: VirtualTerminal, row: number, col: number): number {
|
||||
const xterm = (terminal as unknown as { xterm: XtermTerminalType }).xterm;
|
||||
const xterm = getXterm(terminal);
|
||||
const buffer = xterm.buffer.active;
|
||||
const line = buffer.getLine(buffer.viewportY + row);
|
||||
assert.ok(line, `Missing buffer line at row ${row}`);
|
||||
@@ -402,8 +406,8 @@ describe("TUI resize handling", () => {
|
||||
});
|
||||
|
||||
describe("TUI content shrinkage", () => {
|
||||
it("clears empty rows when content shrinks significantly", async () => {
|
||||
const terminal = new VirtualTerminal(40, 10);
|
||||
it("clears empty rows differentially when content shrinks significantly", async () => {
|
||||
const terminal = new LoggingVirtualTerminal(40, 10);
|
||||
const tui = new TUI(terminal);
|
||||
tui.setClearOnShrink(true); // Explicitly enable (may be disabled via env var)
|
||||
const component = new TestComponent();
|
||||
@@ -415,14 +419,16 @@ describe("TUI content shrinkage", () => {
|
||||
await terminal.waitForRender();
|
||||
|
||||
const initialRedraws = tui.fullRedraws;
|
||||
terminal.clearWrites();
|
||||
|
||||
// Shrink to fewer lines
|
||||
component.lines = ["Line 0", "Line 1"];
|
||||
tui.requestRender();
|
||||
await terminal.waitForRender();
|
||||
|
||||
// Should have triggered a full redraw to clear empty rows
|
||||
assert.ok(tui.fullRedraws > initialRedraws, "Content shrinkage should trigger full redraw");
|
||||
assert.strictEqual(tui.fullRedraws, initialRedraws, "Content shrinkage should stay on the differential path");
|
||||
assert.ok(!terminal.getWrites().includes("\x1b[2J"), "Differential shrink should not clear the whole screen");
|
||||
assert.ok(!terminal.getWrites().includes("\x1b[3J"), "Differential shrink should not clear scrollback");
|
||||
|
||||
const viewport = terminal.getViewport();
|
||||
assert.ok(viewport[0]?.includes("Line 0"), "First line preserved");
|
||||
@@ -434,6 +440,93 @@ describe("TUI content shrinkage", () => {
|
||||
tui.stop();
|
||||
});
|
||||
|
||||
it("preserves detached scrollback viewport when clearOnShrink redraws the viewport", async () => {
|
||||
const terminal = new LoggingVirtualTerminal(20, 5);
|
||||
const tui = new TUI(terminal);
|
||||
tui.setClearOnShrink(true);
|
||||
const component = new TestComponent();
|
||||
tui.addChild(component);
|
||||
|
||||
component.lines = Array.from({ length: 20 }, (_, i) => `Line ${i}`);
|
||||
tui.start();
|
||||
await terminal.waitForRender();
|
||||
|
||||
const initialRedraws = tui.fullRedraws;
|
||||
terminal.clearWrites();
|
||||
const xterm = getXterm(terminal);
|
||||
xterm.scrollLines(-10);
|
||||
const viewportYBefore = xterm.buffer.active.viewportY;
|
||||
const viewportBefore = terminal.getViewport();
|
||||
|
||||
component.lines = Array.from({ length: 18 }, (_, i) => `Line ${i}`);
|
||||
tui.requestRender();
|
||||
await terminal.waitForRender();
|
||||
|
||||
assert.strictEqual(tui.fullRedraws, initialRedraws, "Tall shrink should avoid the full-render path");
|
||||
assert.ok(!terminal.getWrites().includes("\x1b[2J"), "Tall shrink should not clear the whole screen");
|
||||
assert.ok(!terminal.getWrites().includes("\x1b[3J"), "Tall shrink should not clear scrollback");
|
||||
assert.strictEqual(
|
||||
xterm.buffer.active.viewportY,
|
||||
viewportYBefore,
|
||||
"Detached viewport position should be preserved",
|
||||
);
|
||||
assert.deepStrictEqual(terminal.getViewport(), viewportBefore);
|
||||
|
||||
tui.stop();
|
||||
});
|
||||
|
||||
it("keeps the bottom pinned when clearOnShrink redraws the viewport", async () => {
|
||||
const terminal = new LoggingVirtualTerminal(20, 5);
|
||||
const tui = new TUI(terminal);
|
||||
tui.setClearOnShrink(true);
|
||||
const component = new TestComponent();
|
||||
tui.addChild(component);
|
||||
|
||||
component.lines = Array.from({ length: 20 }, (_, i) => `Line ${i}`);
|
||||
tui.start();
|
||||
await terminal.waitForRender();
|
||||
|
||||
const initialRedraws = tui.fullRedraws;
|
||||
terminal.clearWrites();
|
||||
|
||||
component.lines = Array.from({ length: 18 }, (_, i) => `Line ${i}`);
|
||||
tui.requestRender();
|
||||
await terminal.waitForRender();
|
||||
|
||||
assert.strictEqual(tui.fullRedraws, initialRedraws, "Tall shrink should avoid the full-render path");
|
||||
assert.ok(!terminal.getWrites().includes("\x1b[2J"), "Tall shrink should not clear the whole screen");
|
||||
assert.ok(!terminal.getWrites().includes("\x1b[3J"), "Tall shrink should not clear scrollback");
|
||||
assert.deepStrictEqual(terminal.getViewport(), ["Line 13", "Line 14", "Line 15", "Line 16", "Line 17"]);
|
||||
|
||||
tui.stop();
|
||||
});
|
||||
|
||||
it("redraws shrink viewport rows independently when lines fill the terminal width", async () => {
|
||||
const terminal = new LoggingVirtualTerminal(20, 5);
|
||||
const tui = new TUI(terminal);
|
||||
tui.setClearOnShrink(true);
|
||||
const component = new TestComponent();
|
||||
tui.addChild(component);
|
||||
const makeLine = (index: number): string => `Line ${index.toString().padStart(2, "0")} ${"x".repeat(12)}`;
|
||||
|
||||
component.lines = Array.from({ length: 20 }, (_, i) => makeLine(i));
|
||||
tui.start();
|
||||
await terminal.waitForRender();
|
||||
terminal.clearWrites();
|
||||
|
||||
component.lines = Array.from({ length: 18 }, (_, i) => makeLine(i));
|
||||
tui.requestRender();
|
||||
await terminal.waitForRender();
|
||||
|
||||
assert.ok(!terminal.getWrites().includes("\r\n"), "Viewport redraw should not depend on newline row advances");
|
||||
assert.deepStrictEqual(
|
||||
terminal.getViewport(),
|
||||
[13, 14, 15, 16, 17].map((index) => makeLine(index)),
|
||||
);
|
||||
|
||||
tui.stop();
|
||||
});
|
||||
|
||||
it("handles shrink to single line", async () => {
|
||||
const terminal = new VirtualTerminal(40, 10);
|
||||
const tui = new TUI(terminal);
|
||||
|
||||
Reference in New Issue
Block a user