mirror of
https://github.com/aphrody-labs/opentui.git
synced 2026-10-13 21:08:53 +00:00
renderer: fix split-footer resize cleanup (#1488)
A width shrink could clear below a settling footer, leaving stale rows visible. Clear from the visible footer origin while preserving pinned cleanup. Supersede #1487 Fix #1486
This commit is contained in:
1 parent
97a20ab249
commit
ac753b48d3
2 files changed
+97
-3
No files matched your search
@@ -3996,12 +3996,17 @@ export class CliRenderer extends EventEmitter implements RenderContext {
|
||||
const splitFooterActive = this._screenMode === "split-footer"
|
||||
|
||||
if (splitFooterActive) {
|
||||
// Width shrink historically needs a broader scrub band, but if resize interrupts
|
||||
// a deferred footer transition we also need to clear from that visible source surface.
|
||||
// Width shrink historically needs a broader bottom-anchored scrub band so wrapped
|
||||
// footer cells above a pinned surface are erased. Never start that clear below the
|
||||
// visible footer origin, or a settling footer leaves stale rows. Interrupted
|
||||
// transitions may still have an earlier source that must also be included.
|
||||
let clearStart: number | null = null
|
||||
|
||||
if (width < prevWidth && visiblePreviousSplitHeight > 0) {
|
||||
clearStart = Math.max(previousTerminalHeight - visiblePreviousSplitHeight * 2, 1)
|
||||
clearStart = Math.min(
|
||||
this.renderOffset + 1,
|
||||
Math.max(previousTerminalHeight - visiblePreviousSplitHeight * 2, 1),
|
||||
)
|
||||
}
|
||||
|
||||
if (pendingSplitFooterTransition !== null) {
|
||||
|
||||
@@ -1774,6 +1774,95 @@ test("CliRenderer split-footer resize cleanup uses the visible footer surface wh
|
||||
writeOutSpy.mockRestore()
|
||||
})
|
||||
|
||||
test("CliRenderer split-footer width shrink clears from the visible settling footer origin", async () => {
|
||||
const result = await createTestRenderer({
|
||||
width: 80,
|
||||
height: 24,
|
||||
screenMode: "split-footer",
|
||||
footerHeight: 6,
|
||||
externalOutputMode: "capture-stdout",
|
||||
consoleMode: "disabled",
|
||||
})
|
||||
|
||||
renderer = result.renderer
|
||||
;(renderer as any)._terminalIsSetup = true
|
||||
|
||||
expect((renderer as any).renderOffset).toBe(1)
|
||||
|
||||
const writeOutSpy = spyOn(renderer as any, "writeOut")
|
||||
|
||||
result.resize(44, 22)
|
||||
|
||||
expect(writeOutSpy).toHaveBeenCalledTimes(1)
|
||||
expect(writeOutSpy.mock.calls[0]?.[0]).toBe(ANSI.moveCursorAndClear(2, 1))
|
||||
|
||||
writeOutSpy.mockRestore()
|
||||
})
|
||||
|
||||
test("CliRenderer split-footer width shrink clears from a mid-settling footer origin above the bottom-anchored band", async () => {
|
||||
const result = await createTestRenderer({
|
||||
width: 80,
|
||||
height: 24,
|
||||
screenMode: "split-footer",
|
||||
footerHeight: 6,
|
||||
externalOutputMode: "capture-stdout",
|
||||
consoleMode: "disabled",
|
||||
})
|
||||
|
||||
renderer = result.renderer
|
||||
;(renderer as any)._terminalIsSetup = true
|
||||
|
||||
;(renderer as any).stdout.write("a\n")
|
||||
await result.renderOnce()
|
||||
;(renderer as any).stdout.write("b\n")
|
||||
await result.renderOnce()
|
||||
;(renderer as any).stdout.write("c\n")
|
||||
await result.renderOnce()
|
||||
;(renderer as any).stdout.write("d\n")
|
||||
await result.renderOnce()
|
||||
|
||||
expect((renderer as any).renderOffset).toBe(5)
|
||||
|
||||
const writeOutSpy = spyOn(renderer as any, "writeOut")
|
||||
|
||||
result.resize(44, 24)
|
||||
|
||||
expect(writeOutSpy).toHaveBeenCalledTimes(1)
|
||||
expect(writeOutSpy.mock.calls[0]?.[0]).toBe(ANSI.moveCursorAndClear(6, 1))
|
||||
|
||||
writeOutSpy.mockRestore()
|
||||
})
|
||||
|
||||
test("CliRenderer split-footer width shrink retains the broader scrub band when pinned", async () => {
|
||||
const result = await createTestRenderer({
|
||||
width: 80,
|
||||
height: 24,
|
||||
screenMode: "split-footer",
|
||||
footerHeight: 6,
|
||||
externalOutputMode: "capture-stdout",
|
||||
consoleMode: "disabled",
|
||||
})
|
||||
|
||||
renderer = result.renderer
|
||||
;(renderer as any)._terminalIsSetup = true
|
||||
|
||||
for (let i = 0; i < 20; i++) {
|
||||
;(renderer as any).stdout.write(`line-${i}\n`)
|
||||
await result.renderOnce()
|
||||
}
|
||||
|
||||
expect((renderer as any).renderOffset).toBe(18)
|
||||
|
||||
const writeOutSpy = spyOn(renderer as any, "writeOut")
|
||||
|
||||
result.resize(44, 24)
|
||||
|
||||
expect(writeOutSpy).toHaveBeenCalledTimes(1)
|
||||
expect(writeOutSpy.mock.calls[0]?.[0]).toBe(ANSI.moveCursorAndClear(12, 1))
|
||||
|
||||
writeOutSpy.mockRestore()
|
||||
})
|
||||
|
||||
test("CliRenderer split-footer resize cleanup uses the visible source top line across width and height resize while a deferred footer transition is pending", async () => {
|
||||
const result = await createTestRenderer({
|
||||
width: 40,
|
||||
|
||||
Reference in new issue
Block a user