fix(tui): cursor position tracking when content shrinks with unchanged lines

This commit is contained in:
Mario Zechner 2026-01-11 03:00:23 +01:00
parent d7394eb109
commit 3bb115a39c
3 changed files with 69 additions and 0 deletions

View file

@ -4,6 +4,7 @@
### Fixed
- Cursor position tracking when content shrinks with unchanged remaining lines
- TUI renders with wrong dimensions after suspend/resume if terminal was resized while suspended ([#599](https://github.com/badlogic/pi-mono/issues/599))
## [0.42.4] - 2026-01-10

View file

@ -385,6 +385,31 @@ export class TUI extends Container {
return;
}
// All changes are in deleted lines (nothing to render, just clear)
if (firstChanged >= newLines.length) {
if (this.previousLines.length > newLines.length) {
let buffer = "\x1b[?2026h";
// Move to end of new content
const targetRow = newLines.length - 1;
const lineDiff = targetRow - this.cursorRow;
if (lineDiff > 0) buffer += `\x1b[${lineDiff}B`;
else if (lineDiff < 0) buffer += `\x1b[${-lineDiff}A`;
buffer += "\r";
// Clear extra lines
const extraLines = this.previousLines.length - newLines.length;
for (let i = 0; i < extraLines; i++) {
buffer += "\r\n\x1b[2K";
}
buffer += `\x1b[${extraLines}A`;
buffer += "\x1b[?2026l";
this.terminal.write(buffer);
this.cursorRow = newLines.length - 1;
}
this.previousLines = newLines;
this.previousWidth = width;
return;
}
// Check if firstChanged is outside the viewport
// cursorRow is the line where cursor is (0-indexed)
// Viewport shows lines from (cursorRow - height + 1) to cursorRow

View file

@ -0,0 +1,43 @@
import assert from "node:assert";
import { describe, it } from "node:test";
import { type Component, TUI } from "../src/tui.js";
import { VirtualTerminal } from "./virtual-terminal.js";
class TestComponent implements Component {
lines: string[] = [];
render(_width: number): string[] {
return this.lines;
}
invalidate(): void {}
}
describe("TUI differential rendering", () => {
it("tracks cursor correctly when content shrinks with unchanged remaining lines", async () => {
const terminal = new VirtualTerminal(40, 10);
const tui = new TUI(terminal);
const component = new TestComponent();
tui.addChild(component);
// Initial render: 5 identical lines
component.lines = ["Line 0", "Line 1", "Line 2", "Line 3", "Line 4"];
tui.start();
await terminal.flush();
// Shrink to 3 lines, all identical to before (no content changes in remaining lines)
component.lines = ["Line 0", "Line 1", "Line 2"];
tui.requestRender();
await terminal.flush();
// cursorRow should be 2 (last line of new content)
// Verify by doing another render with a change on line 1
component.lines = ["Line 0", "CHANGED", "Line 2"];
tui.requestRender();
await terminal.flush();
const viewport = terminal.getViewport();
// Line 1 should show "CHANGED", proving cursor tracking was correct
assert.ok(viewport[1]?.includes("CHANGED"), `Expected "CHANGED" on line 1, got: ${viewport[1]}`);
tui.stop();
});
});