diff --git a/src/ink.tsx b/src/ink.tsx
index 1162777a9..bce2d5497 100644
--- a/src/ink.tsx
+++ b/src/ink.tsx
@@ -175,8 +175,10 @@ export default class Ink {
if (outputHeight >= this.options.stdout.rows) {
this.options.stdout.write(
- ansiEscapes.clearTerminal + this.fullStaticOutput + output
+ ansiEscapes.clearTerminal + this.fullStaticOutput
);
+
+ this.log(output, {force: true, erase: false});
this.lastOutput = output;
return;
}
diff --git a/src/log-update.ts b/src/log-update.ts
index a38e565a3..2b46a0322 100644
--- a/src/log-update.ts
+++ b/src/log-update.ts
@@ -5,7 +5,7 @@ import cliCursor from 'cli-cursor';
export type LogUpdate = {
clear: () => void;
done: () => void;
- (str: string): void;
+ (str: string, options?: {force?: boolean; erase?: boolean}): void;
};
const create = (stream: Writable, {showCursor = false} = {}): LogUpdate => {
@@ -13,19 +13,25 @@ const create = (stream: Writable, {showCursor = false} = {}): LogUpdate => {
let previousOutput = '';
let hasHiddenCursor = false;
- const render = (str: string) => {
+ const render = (
+ str: string,
+ {force = false, erase = true}: {force?: boolean; erase?: boolean} = {}
+ ) => {
if (!showCursor && !hasHiddenCursor) {
cliCursor.hide();
hasHiddenCursor = true;
}
const output = str + '\n';
- if (output === previousOutput) {
+ if (output === previousOutput && !force) {
return;
}
previousOutput = output;
- stream.write(ansiEscapes.eraseLines(previousLineCount) + output);
+
+ const eraser = erase ? ansiEscapes.eraseLines(previousLineCount) : '';
+ stream.write(eraser + output);
+
previousLineCount = output.split('\n').length;
};
diff --git a/test/fixtures/erase-once-with-static.tsx b/test/fixtures/erase-once-with-static.tsx
new file mode 100644
index 000000000..9dfac4d72
--- /dev/null
+++ b/test/fixtures/erase-once-with-static.tsx
@@ -0,0 +1,33 @@
+import process from 'node:process';
+import React, {useState} from 'react';
+import {Box, Static, Text, render, useInput} from '../../src/index.js';
+
+function Test() {
+ const [fullHeight, setFullHeight] = useState(true);
+
+ useInput(
+ input => {
+ if (input === 'x') {
+ setFullHeight(false);
+ }
+ },
+ {isActive: fullHeight}
+ );
+
+ return (
+ <>
+
+ {item => {item}}
+
+
+
+ A
+ B
+ {fullHeight && C}
+
+ >
+ );
+}
+
+process.stdout.rows = Number(process.argv[2]);
+render();
diff --git a/test/fixtures/erase-once.tsx b/test/fixtures/erase-once.tsx
new file mode 100644
index 000000000..267702c0e
--- /dev/null
+++ b/test/fixtures/erase-once.tsx
@@ -0,0 +1,27 @@
+import process from 'node:process';
+import React, {useState} from 'react';
+import {Box, Text, render, useInput} from '../../src/index.js';
+
+function Test() {
+ const [fullHeight, setFullHeight] = useState(true);
+
+ useInput(
+ input => {
+ if (input === 'x') {
+ setFullHeight(false);
+ }
+ },
+ {isActive: fullHeight}
+ );
+
+ return (
+
+ A
+ B
+ {fullHeight && C}
+
+ );
+}
+
+process.stdout.rows = Number(process.argv[2]);
+render();
diff --git a/test/render.tsx b/test/render.tsx
index 8ce8333bd..c89ea3f33 100644
--- a/test/render.tsx
+++ b/test/render.tsx
@@ -106,6 +106,54 @@ test.serial('erase screen', async t => {
}
});
+test.serial('erase screen once then continue rendering as usual', async t => {
+ const ps = term('erase-once', ['3']);
+ await delay(1000);
+
+ t.true(ps.output.includes(ansiEscapes.clearTerminal));
+ t.true(ps.output.includes('A'));
+ t.true(ps.output.includes('B'));
+ t.true(ps.output.includes('C'));
+
+ ps.output = '';
+ ps.write('x');
+
+ await ps.waitForExit();
+
+ t.false(ps.output.includes(ansiEscapes.clearTerminal));
+ t.true(ps.output.includes(ansiEscapes.eraseLines(3)));
+ t.true(ps.output.includes('A'));
+ t.true(ps.output.includes('B'));
+ t.false(ps.output.includes('C'));
+});
+
+test.serial(
+ 'erase screen once then continue rendering as usual with present',
+ async t => {
+ const ps = term('erase-once-with-static', ['3']);
+ await delay(1000);
+
+ t.true(ps.output.includes(ansiEscapes.clearTerminal));
+ t.true(ps.output.includes('X'));
+ t.true(ps.output.includes('Y'));
+ t.true(ps.output.includes('Z'));
+ t.true(ps.output.includes('A'));
+ t.true(ps.output.includes('B'));
+ t.true(ps.output.includes('C'));
+
+ ps.output = '';
+ ps.write('x');
+
+ await ps.waitForExit();
+
+ t.false(ps.output.includes(ansiEscapes.clearTerminal));
+ t.true(ps.output.includes(ansiEscapes.eraseLines(2)));
+ t.true(ps.output.includes('A'));
+ t.true(ps.output.includes('B'));
+ t.false(ps.output.includes('C'));
+ }
+);
+
test.serial(
'erase screen where exists but interactive part is taller than viewport',
async t => {