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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy