fix(tui): preserved tmux viewport on offscreen shrink
Skipped repainting tmux panes when an offscreen shrink leaves the visible tail unchanged, and added regression coverage for the no-content-bytes contract. Fixes #2046
This commit is contained in:
@@ -29,6 +29,8 @@
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed tmux offscreen-shrink frames to skip repainting when the visible tail is unchanged, avoiding intermittent blank/refresh flashes in pane terminals ([#2046](https://github.com/can1357/oh-my-pi/issues/2046)).
|
||||
|
||||
- Fixed `Loader` text updates to skip identical messages and preserve the rendered `Text` cache instead of invalidating it every timer tick.
|
||||
|
||||
- Fixed fullscreen overlay alt-frame rendering to reuse the current line-preparation path instead of calling removed fitting helpers.
|
||||
|
||||
+14
-1
@@ -2049,7 +2049,9 @@ export class TUI extends Container {
|
||||
newLines.length < this.#previousLines.length &&
|
||||
naturalViewportTop !== prevViewportTop
|
||||
) {
|
||||
return { kind: "viewportRepaint" };
|
||||
return this.#bottomAnchoredViewportUnchanged(newLines, height)
|
||||
? { kind: "deferredMutation" }
|
||||
: { kind: "viewportRepaint" };
|
||||
}
|
||||
|
||||
// Direct-input shrink can also move the natural viewport upward even when
|
||||
@@ -2437,6 +2439,17 @@ export class TUI extends Container {
|
||||
return { kind: "liveRegionPinned", appendFrom, appendTo, renderViewportTop };
|
||||
}
|
||||
|
||||
#bottomAnchoredViewportUnchanged(newLines: string[], height: number): boolean {
|
||||
const previousViewportTop = Math.max(0, this.#previousLines.length - height);
|
||||
const newViewportTop = Math.max(0, newLines.length - height);
|
||||
for (let row = 0; row < height; row++) {
|
||||
if ((newLines[newViewportTop + row] ?? "") !== (this.#previousLines[previousViewportTop + row] ?? "")) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
#planDeferredTailRepaint(newLines: string[], prevViewportTop: number, height: number): RenderIntent {
|
||||
const row = prevViewportTop + height - 1;
|
||||
if (row < 0 || row >= this.#previousLines.length || newLines.length !== this.#previousLines.length) {
|
||||
|
||||
@@ -1469,6 +1469,40 @@ describe("TUI terminal-state regressions", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("tmux: offscreen shrink preserving the visible tail emits no repaint bytes", async () => {
|
||||
await withEnvPatch({ TMUX: "1", STY: undefined, ZELLIJ: undefined }, async () => {
|
||||
const term = new UnknownViewportTerminal(40, 4, 10_000);
|
||||
const tui = new TUI(term);
|
||||
const component = new MutableLinesComponent([
|
||||
"old-0",
|
||||
"remove-me",
|
||||
"old-2",
|
||||
"old-3",
|
||||
"tail-0",
|
||||
"tail-1",
|
||||
"tail-2",
|
||||
"tail-3",
|
||||
]);
|
||||
tui.addChild(component);
|
||||
|
||||
try {
|
||||
tui.start();
|
||||
await settle(term);
|
||||
expect(visible(term)).toEqual(["tail-0", "tail-1", "tail-2", "tail-3"]);
|
||||
|
||||
const writes = captureWrites(term);
|
||||
component.setLines(["old-0", "old-2", "old-3", "tail-0", "tail-1", "tail-2", "tail-3"]);
|
||||
tui.requestRender();
|
||||
await settle(term);
|
||||
|
||||
expect(visible(term)).toEqual(["tail-0", "tail-1", "tail-2", "tail-3"]);
|
||||
expect(writes).toEqual([]);
|
||||
} finally {
|
||||
tui.stop();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Root cause family: the dirty/replay machinery assumes native scrollback
|
||||
// can be cleared and rebuilt, which is never true inside a multiplexer —
|
||||
// tmux owns pane history, reflows it on resize itself, and a "replay" can
|
||||
|
||||
Reference in New Issue
Block a user