Skip to content

Commit 8f2ad8f

Browse files
authored
Fix stale output when outputHeight changes from above stdout.rows to below stdout.rows (#717)
1 parent b9e9466 commit 8f2ad8f

File tree

4 files changed

+70
-2
lines changed

4 files changed

+70
-2
lines changed

src/ink.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export default class Ink {
3535
// Ignore last render after unmounting a tree to prevent empty output before exit
3636
private isUnmounted: boolean;
3737
private lastOutput: string;
38+
private lastOutputHeight: number;
3839
private readonly container: FiberRoot;
3940
private readonly rootNode: dom.DOMElement;
4041
// This variable is used only in debug mode to store full static output
@@ -72,6 +73,7 @@ export default class Ink {
7273

7374
// Store last output to only rerender when needed
7475
this.lastOutput = '';
76+
this.lastOutputHeight = 0;
7577

7678
// This variable is used only in debug mode to store full static output
7779
// so that it's rerendered every time, not just new static parts, like in non-debug mode
@@ -168,18 +170,21 @@ export default class Ink {
168170
}
169171

170172
this.lastOutput = output;
173+
this.lastOutputHeight = outputHeight;
171174
return;
172175
}
173176

174177
if (hasStaticOutput) {
175178
this.fullStaticOutput += staticOutput;
176179
}
177180

178-
if (outputHeight >= this.options.stdout.rows) {
181+
if (this.lastOutputHeight >= this.options.stdout.rows) {
179182
this.options.stdout.write(
180-
ansiEscapes.clearTerminal + this.fullStaticOutput + output,
183+
ansiEscapes.clearTerminal + this.fullStaticOutput + output + '\n',
181184
);
182185
this.lastOutput = output;
186+
this.lastOutputHeight = outputHeight;
187+
this.log.sync(output);
183188
return;
184189
}
185190

@@ -195,6 +200,7 @@ export default class Ink {
195200
}
196201

197202
this.lastOutput = output;
203+
this.lastOutputHeight = outputHeight;
198204
};
199205

200206
render(node: ReactNode): void {

src/log-update.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import cliCursor from 'cli-cursor';
55
export type LogUpdate = {
66
clear: () => void;
77
done: () => void;
8+
sync: (str: string) => void;
89
(str: string): void;
910
};
1011

@@ -45,6 +46,12 @@ const create = (stream: Writable, {showCursor = false} = {}): LogUpdate => {
4546
}
4647
};
4748

49+
render.sync = (str: string) => {
50+
const output = str + '\n';
51+
previousOutput = output;
52+
previousLineCount = output.split('\n').length;
53+
};
54+
4855
return render;
4956
};
5057

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import process from 'node:process';
2+
import React, {useEffect, useState} from 'react';
3+
import {Box, Text, render} from '../../src/index.js';
4+
5+
function Erase() {
6+
const [show, setShow] = useState(true);
7+
8+
useEffect(() => {
9+
const timer = setTimeout(() => {
10+
setShow(false);
11+
});
12+
13+
return () => {
14+
clearTimeout(timer);
15+
};
16+
}, []);
17+
18+
return (
19+
<Box flexDirection="column">
20+
{show && (
21+
<>
22+
<Text>A</Text>
23+
<Text>B</Text>
24+
<Text>C</Text>
25+
</>
26+
)}
27+
</Box>
28+
);
29+
}
30+
31+
process.stdout.rows = Number(process.argv[2]);
32+
render(<Erase />);

test/render.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,29 @@ test.serial(
120120
},
121121
);
122122

123+
test.serial('erase screen where state changes', async t => {
124+
const ps = term('erase-with-state-change', ['4']);
125+
await ps.waitForExit();
126+
127+
const secondFrame = ps.output.split(ansiEscapes.eraseLines(3))[1];
128+
129+
for (const letter of ['A', 'B', 'C']) {
130+
t.false(secondFrame?.includes(letter));
131+
}
132+
});
133+
134+
test.serial('erase screen where state changes in small viewport', async t => {
135+
const ps = term('erase-with-state-change', ['3']);
136+
await ps.waitForExit();
137+
138+
const frames = ps.output.split(ansiEscapes.clearTerminal);
139+
const lastFrame = frames.at(-1);
140+
141+
for (const letter of ['A', 'B', 'C']) {
142+
t.false(lastFrame?.includes(letter));
143+
}
144+
});
145+
123146
test.serial('clear output', async t => {
124147
const ps = term('clear');
125148
await ps.waitForExit();

0 commit comments

Comments
 (0)