Skip to content

Commit

Permalink
Fix rendering when terminal is erased on first render
Browse files Browse the repository at this point in the history
  • Loading branch information
vadimdemedes committed May 3, 2023
1 parent 8a04760 commit 547f547
Show file tree
Hide file tree
Showing 5 changed files with 121 additions and 5 deletions.
4 changes: 3 additions & 1 deletion src/ink.tsx
Expand Up @@ -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;
}
Expand Down
14 changes: 10 additions & 4 deletions src/log-update.ts
Expand Up @@ -5,27 +5,33 @@ 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 => {
let previousLineCount = 0;
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;
};

Expand Down
33 changes: 33 additions & 0 deletions 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 (
<>
<Static items={['X', 'Y', 'Z']}>
{item => <Text key={item}>{item}</Text>}
</Static>

<Box flexDirection="column">
<Text>A</Text>
<Text>B</Text>
{fullHeight && <Text>C</Text>}
</Box>
</>
);
}

process.stdout.rows = Number(process.argv[2]);
render(<Test />);
27 changes: 27 additions & 0 deletions 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 (
<Box flexDirection="column">
<Text>A</Text>
<Text>B</Text>
{fullHeight && <Text>C</Text>}
</Box>
);
}

process.stdout.rows = Number(process.argv[2]);
render(<Test />);
48 changes: 48 additions & 0 deletions test/render.tsx
Expand Up @@ -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 <Static> 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 <Static> exists but interactive part is taller than viewport',
async t => {
Expand Down

0 comments on commit 547f547

Please sign in to comment.