fix(tui): auto-clear empty rows when content shrinks (#1095)

- Add height change detection (analogous to existing width detection)
- Auto-detect when content shrinks below maxLinesRendered and trigger
  full re-render to clear leftover empty rows
- Only triggers when no overlays are active (overlays need padding)

Fixes empty rows appearing below footer when:
- Closing /tree or other selectors
- Clearing multi-line editor content
- Any component shrinking

Also adds regression tests for resize handling and content shrinkage.
This commit is contained in:
Marc Krenn 2026-01-30 20:45:34 +01:00 committed by GitHub
parent bb0c2bf77a
commit 39c898d7c9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 144 additions and 4 deletions

View file

@ -199,6 +199,7 @@ export class TUI extends Container {
public terminal: Terminal;
private previousLines: string[] = [];
private previousWidth = 0;
private previousHeight = 0;
private focusedComponent: Component | null = null;
/** Global callback for debug key (Shift+Ctrl+D). Called before input is forwarded to focused component. */
@ -396,6 +397,7 @@ export class TUI extends Container {
if (force) {
this.previousLines = [];
this.previousWidth = -1; // -1 triggers widthChanged, forcing a full clear
this.previousHeight = -1; // -1 triggers heightChanged, forcing a full clear
this.cursorRow = 0;
this.hardwareCursorRow = 0;
this.maxLinesRendered = 0;
@ -825,8 +827,9 @@ export class TUI extends Container {
newLines = this.applyLineResets(newLines);
// Width changed - need full re-render
// 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 => {
@ -851,16 +854,24 @@ export class TUI extends Container {
this.positionHardwareCursor(cursorPos, newLines.length);
this.previousLines = newLines;
this.previousWidth = width;
this.previousHeight = height;
};
// First render - just output everything without clearing (assumes clean screen)
if (this.previousLines.length === 0 && !widthChanged) {
if (this.previousLines.length === 0 && !widthChanged && !heightChanged) {
fullRender(false);
return;
}
// Width changed - full re-render
if (widthChanged) {
// Width or height changed - full re-render
if (widthChanged || heightChanged) {
fullRender(true);
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)
if (newLines.length < this.maxLinesRendered && this.overlayStack.length === 0) {
fullRender(true);
return;
}
@ -930,6 +941,7 @@ export class TUI extends Container {
this.positionHardwareCursor(cursorPos, newLines.length);
this.previousLines = newLines;
this.previousWidth = width;
this.previousHeight = height;
this.previousViewportTop = Math.max(0, this.maxLinesRendered - height);
return;
}
@ -1076,6 +1088,7 @@ export class TUI extends Container {
this.previousLines = newLines;
this.previousWidth = width;
this.previousHeight = height;
}
/**