fix(tui): reset viewport state after shrink

This commit is contained in:
Mario Zechner
2026-03-26 10:40:10 +01:00
parent 7e94d36a44
commit cb4e4d8c9a
3 changed files with 128 additions and 16 deletions

View File

@@ -874,8 +874,11 @@ export class TUI extends Container {
if (this.stopped) return;
const width = this.terminal.columns;
const height = this.terminal.rows;
let viewportTop = Math.max(0, this.maxLinesRendered - height);
let prevViewportTop = this.previousViewportTop;
const widthChanged = this.previousWidth !== 0 && this.previousWidth !== width;
const heightChanged = this.previousHeight !== 0 && this.previousHeight !== height;
const previousBufferLength = this.previousHeight > 0 ? this.previousViewportTop + this.previousHeight : height;
let prevViewportTop = heightChanged ? Math.max(0, previousBufferLength - height) : this.previousViewportTop;
let viewportTop = prevViewportTop;
let hardwareCursorRow = this.hardwareCursorRow;
const computeLineDiff = (targetRow: number): number => {
const currentScreenRow = hardwareCursorRow - prevViewportTop;
@@ -896,10 +899,6 @@ export class TUI extends Container {
newLines = this.applyLineResets(newLines);
// Width or height changed - need full re-render
const widthChanged = this.previousWidth !== 0 && this.previousWidth !== width;
const heightChanged = this.previousHeight !== 0 && this.previousHeight !== height;
// Helper to clear scrollback and viewport and render all new lines
const fullRender = (clear: boolean): void => {
this.fullRedrawCount += 1;
@@ -919,7 +918,8 @@ export class TUI extends Container {
} else {
this.maxLinesRendered = Math.max(this.maxLinesRendered, newLines.length);
}
this.previousViewportTop = Math.max(0, this.maxLinesRendered - height);
const bufferLength = Math.max(height, newLines.length);
this.previousViewportTop = Math.max(0, bufferLength - height);
this.positionHardwareCursor(cursorPos, newLines.length);
this.previousLines = newLines;
this.previousWidth = width;
@@ -993,7 +993,7 @@ export class TUI extends Container {
// No changes - but still need to update hardware cursor position if it moved
if (firstChanged === -1) {
this.positionHardwareCursor(cursorPos, newLines.length);
this.previousViewportTop = Math.max(0, this.maxLinesRendered - height);
this.previousViewportTop = prevViewportTop;
this.previousHeight = height;
return;
}
@@ -1004,6 +1004,11 @@ export class TUI extends Container {
let buffer = "\x1b[?2026h";
// Move to end of new content (clamp to 0 for empty content)
const targetRow = Math.max(0, newLines.length - 1);
if (targetRow < prevViewportTop) {
logRedraw(`deleted lines moved viewport up (${targetRow} < ${prevViewportTop})`);
fullRender(true);
return;
}
const lineDiff = computeLineDiff(targetRow);
if (lineDiff > 0) buffer += `\x1b[${lineDiff}B`;
else if (lineDiff < 0) buffer += `\x1b[${-lineDiff}A`;
@@ -1034,16 +1039,14 @@ export class TUI extends Container {
this.previousLines = newLines;
this.previousWidth = width;
this.previousHeight = height;
this.previousViewportTop = Math.max(0, this.maxLinesRendered - height);
this.previousViewportTop = prevViewportTop;
return;
}
// Check if firstChanged is above what was previously visible
// Use previousLines.length (not maxLinesRendered) to avoid false positives after content shrinks
const previousContentViewportTop = Math.max(0, this.previousLines.length - height);
if (firstChanged < previousContentViewportTop) {
// First change is above previous viewport - need full re-render
logRedraw(`firstChanged < viewportTop (${firstChanged} < ${previousContentViewportTop})`);
// Differential rendering can only touch what was actually visible.
// 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);
return;
}
@@ -1175,7 +1178,7 @@ export class TUI extends Container {
this.hardwareCursorRow = finalCursorRow;
// Track terminal's working area (grows but doesn't shrink unless cleared)
this.maxLinesRendered = Math.max(this.maxLinesRendered, newLines.length);
this.previousViewportTop = Math.max(0, this.maxLinesRendered - height);
this.previousViewportTop = Math.max(prevViewportTop, finalCursorRow - height + 1);
// Position hardware cursor for IME
this.positionHardwareCursor(cursorPos, newLines.length);