fix(tui): skip Termux height redraws closes #2467
This commit is contained in:
@@ -5,6 +5,7 @@
|
|||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Fixed shared keybinding resolution to stop user overrides from evicting unrelated default shortcuts such as selector confirm and editor cursor keys ([#2455](https://github.com/badlogic/pi-mono/issues/2455))
|
- Fixed shared keybinding resolution to stop user overrides from evicting unrelated default shortcuts such as selector confirm and editor cursor keys ([#2455](https://github.com/badlogic/pi-mono/issues/2455))
|
||||||
|
- Fixed Termux software keyboard height changes from forcing full-screen redraws and replaying TUI history on every toggle ([#2467](https://github.com/badlogic/pi-mono/issues/2467))
|
||||||
|
|
||||||
## [0.61.0] - 2026-03-20
|
## [0.61.0] - 2026-03-20
|
||||||
|
|
||||||
|
|||||||
@@ -107,6 +107,10 @@ function parseSizeValue(value: SizeValue | undefined, referenceSize: number): nu
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isTermuxSession(): boolean {
|
||||||
|
return Boolean(process.env.TERMUX_VERSION);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Options for overlay positioning and sizing.
|
* Options for overlay positioning and sizing.
|
||||||
* Values can be absolute numbers or percentage strings (e.g., "50%").
|
* Values can be absolute numbers or percentage strings (e.g., "50%").
|
||||||
@@ -937,9 +941,18 @@ export class TUI extends Container {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Width or height changed - full re-render
|
// Width changes always need a full re-render because wrapping changes.
|
||||||
if (widthChanged || heightChanged) {
|
if (widthChanged) {
|
||||||
logRedraw(`terminal size changed (${this.previousWidth}x${this.previousHeight} -> ${width}x${height})`);
|
logRedraw(`terminal width changed (${this.previousWidth} -> ${width})`);
|
||||||
|
fullRender(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Height changes normally need a full re-render to keep the visible viewport aligned,
|
||||||
|
// but Termux changes height when the software keyboard shows or hides.
|
||||||
|
// In that environment, a full redraw causes the entire history to replay on every toggle.
|
||||||
|
if (heightChanged && !isTermuxSession()) {
|
||||||
|
logRedraw(`terminal height changed (${this.previousHeight} -> ${height})`);
|
||||||
fullRender(true);
|
fullRender(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,47 @@ class TestComponent implements Component {
|
|||||||
invalidate(): void {}
|
invalidate(): void {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class LoggingVirtualTerminal extends VirtualTerminal {
|
||||||
|
private writes: string[] = [];
|
||||||
|
|
||||||
|
override write(data: string): void {
|
||||||
|
this.writes.push(data);
|
||||||
|
super.write(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
getWrites(): string {
|
||||||
|
return this.writes.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
clearWrites(): void {
|
||||||
|
this.writes = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function withEnv<T>(updates: Record<string, string | undefined>, run: () => Promise<T>): Promise<T> {
|
||||||
|
const previousValues = new Map<string, string | undefined>();
|
||||||
|
for (const [key, value] of Object.entries(updates)) {
|
||||||
|
previousValues.set(key, process.env[key]);
|
||||||
|
if (value === undefined) {
|
||||||
|
delete process.env[key];
|
||||||
|
} else {
|
||||||
|
process.env[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await run();
|
||||||
|
} finally {
|
||||||
|
for (const [key, value] of previousValues) {
|
||||||
|
if (value === undefined) {
|
||||||
|
delete process.env[key];
|
||||||
|
} else {
|
||||||
|
process.env[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getCellItalic(terminal: VirtualTerminal, row: number, col: number): number {
|
function getCellItalic(terminal: VirtualTerminal, row: number, col: number): number {
|
||||||
const xterm = (terminal as unknown as { xterm: XtermTerminalType }).xterm;
|
const xterm = (terminal as unknown as { xterm: XtermTerminalType }).xterm;
|
||||||
const buffer = xterm.buffer.active;
|
const buffer = xterm.buffer.active;
|
||||||
@@ -24,28 +65,59 @@ function getCellItalic(terminal: VirtualTerminal, row: number, col: number): num
|
|||||||
|
|
||||||
describe("TUI resize handling", () => {
|
describe("TUI resize handling", () => {
|
||||||
it("triggers full re-render when terminal height changes", async () => {
|
it("triggers full re-render when terminal height changes", async () => {
|
||||||
const terminal = new VirtualTerminal(40, 10);
|
await withEnv({ TERMUX_VERSION: undefined }, async () => {
|
||||||
const tui = new TUI(terminal);
|
const terminal = new VirtualTerminal(40, 10);
|
||||||
const component = new TestComponent();
|
const tui = new TUI(terminal);
|
||||||
tui.addChild(component);
|
const component = new TestComponent();
|
||||||
|
tui.addChild(component);
|
||||||
|
|
||||||
component.lines = ["Line 0", "Line 1", "Line 2"];
|
component.lines = ["Line 0", "Line 1", "Line 2"];
|
||||||
tui.start();
|
tui.start();
|
||||||
await terminal.flush();
|
await terminal.flush();
|
||||||
|
|
||||||
const initialRedraws = tui.fullRedraws;
|
const initialRedraws = tui.fullRedraws;
|
||||||
|
|
||||||
// Resize height
|
// Resize height
|
||||||
terminal.resize(40, 15);
|
terminal.resize(40, 15);
|
||||||
await terminal.flush();
|
await terminal.flush();
|
||||||
|
|
||||||
// Should have triggered a full redraw
|
// Should have triggered a full redraw
|
||||||
assert.ok(tui.fullRedraws > initialRedraws, "Height change should trigger full redraw");
|
assert.ok(tui.fullRedraws > initialRedraws, "Height change should trigger full redraw");
|
||||||
|
|
||||||
const viewport = terminal.getViewport();
|
const viewport = terminal.getViewport();
|
||||||
assert.ok(viewport[0]?.includes("Line 0"), "Content preserved after height change");
|
assert.ok(viewport[0]?.includes("Line 0"), "Content preserved after height change");
|
||||||
|
|
||||||
tui.stop();
|
tui.stop();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips full re-render on height changes in Termux", async () => {
|
||||||
|
await withEnv({ TERMUX_VERSION: "1" }, async () => {
|
||||||
|
const terminal = new LoggingVirtualTerminal(40, 10);
|
||||||
|
const tui = new TUI(terminal);
|
||||||
|
const component = new TestComponent();
|
||||||
|
tui.addChild(component);
|
||||||
|
|
||||||
|
component.lines = Array.from({ length: 20 }, (_, i) => `Line ${i}`);
|
||||||
|
tui.start();
|
||||||
|
await terminal.flush();
|
||||||
|
terminal.clearWrites();
|
||||||
|
|
||||||
|
const initialRedraws = tui.fullRedraws;
|
||||||
|
for (const height of [15, 8, 14, 11]) {
|
||||||
|
terminal.resize(40, height);
|
||||||
|
await terminal.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.strictEqual(tui.fullRedraws, initialRedraws, "Height change should not trigger full redraw");
|
||||||
|
assert.ok(!terminal.getWrites().includes("\x1b[2J"), "Height change should not clear the screen");
|
||||||
|
assert.ok(!terminal.getWrites().includes("\x1b[3J"), "Height change should not clear scrollback");
|
||||||
|
|
||||||
|
const viewport = terminal.getViewport();
|
||||||
|
assert.ok(viewport.join("\n").includes("Line 19"), "Latest content remains visible after resize");
|
||||||
|
|
||||||
|
tui.stop();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("triggers full re-render when terminal width changes", async () => {
|
it("triggers full re-render when terminal width changes", async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user