Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 45d30ad

Browse files
committed
Fix multiline text exceeding console height leaving garbage when scrolling
Fixes #121
1 parent 79d0339 commit 45d30ad

File tree

4 files changed

+203
-3
lines changed

4 files changed

+203
-3
lines changed

example-overflow.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#!/usr/bin/env node
2+
import process from 'node:process';
3+
import chalk from 'chalk';
4+
import ora from './index.js';
5+
6+
console.log(chalk.bold.cyan('\n📏 Terminal Height Overflow Test - Fixed Version 📏'));
7+
console.log(chalk.gray('This demo shows the fix for issue #121 - multiline content exceeding terminal height.\n'));
8+
9+
// Get terminal dimensions
10+
const rows = process.stderr.rows || 30;
11+
const cols = process.stderr.columns || 80;
12+
13+
console.log(chalk.yellow(`Your terminal: ${rows} rows × ${cols} columns`));
14+
console.log(chalk.green(`Creating spinner with ${rows + 10} lines (exceeds by 10 lines)\n`));
15+
16+
// Create content that exceeds terminal height
17+
const lines = [];
18+
for (let i = 1; i <= rows + 10; i++) {
19+
const emoji = ['🔴', '🟠', '🟡', '🟢', '🔵', '🟣'][i % 6];
20+
lines.push(`${emoji} Line ${String(i).padStart(3, '0')}: Processing item #${i}`);
21+
}
22+
23+
const spinner = ora({
24+
text: lines.join('\n'),
25+
spinner: 'dots',
26+
color: 'cyan',
27+
}).start();
28+
29+
// Update a few times
30+
let updates = 0;
31+
const interval = setInterval(() => {
32+
updates++;
33+
if (updates <= 3) {
34+
spinner.color = ['yellow', 'green', 'magenta'][updates - 1];
35+
spinner.text = `Update ${updates}/3\n${lines.join('\n')}`;
36+
} else {
37+
clearInterval(interval);
38+
spinner.succeed('Done! Content that exceeded terminal height has been properly cleared.');
39+
40+
console.log('\n' + chalk.bold.green('✅ The Fix:'));
41+
console.log(chalk.white('When content exceeds terminal height, ora now:'));
42+
console.log(chalk.gray(' 1. Detects the overflow (lines > terminal rows)'));
43+
console.log(chalk.gray(' 2. Truncates content to fit terminal with message'));
44+
console.log(chalk.gray(' 3. Prevents garbage lines from being written'));
45+
46+
console.log('\n' + chalk.bold.yellow('🔍 Try scrolling up now!'));
47+
console.log(chalk.gray('You should NOT see leftover spinner frames above.'));
48+
console.log(chalk.gray('Content was truncated to prevent overflow.\n'));
49+
}
50+
}, 1000);

index.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export type Options = {
3737
readonly suffixText?: string | SuffixTextGenerator;
3838

3939
/**
40-
The name of one of the provided spinners. See [`example.js`](https://github.com/BendingBender/ora/blob/main/example.js) in this repo if you want to test out different spinners. On Windows (expect for Windows Terminal), it will always use the line spinner as the Windows command-line doesn't have proper Unicode support.
40+
The name of one of the provided spinners. See `example.js` in this repo if you want to test out different spinners. On Windows (except for Windows Terminal), it will always use the line spinner as the Windows command-line doesn't have proper Unicode support.
4141
4242
@default 'dots'
4343

index.js

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -276,8 +276,21 @@ class Ora {
276276
}
277277

278278
this.clear();
279-
this.#stream.write(this.frame());
280-
this.#linesToClear = this.#lineCount;
279+
280+
let frameContent = this.frame();
281+
let actualLineCount = this.#lineCount;
282+
283+
// If content would exceed viewport height, truncate it to prevent garbage
284+
const consoleHeight = this.#stream.rows;
285+
if (consoleHeight && consoleHeight > 1 && this.#lineCount > consoleHeight) {
286+
const lines = frameContent.split('\n');
287+
const maxLines = consoleHeight - 1; // Reserve one line for truncation message
288+
frameContent = [...lines.slice(0, maxLines), '... (content truncated to fit terminal)'].join('\n');
289+
actualLineCount = maxLines + 1; // Truncated lines + message line
290+
}
291+
292+
this.#stream.write(frameContent);
293+
this.#linesToClear = actualLineCount;
281294

282295
return this;
283296
}

test.js

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -983,3 +983,140 @@ Example output:
983983
... 1 more item
984984
]
985985
*/
986+
987+
test('multiline text exceeding console height', t => {
988+
// Create a mock stream with limited height
989+
const stream = getPassThroughStream();
990+
stream.rows = 5; // Simulate a console with 5 rows
991+
stream.columns = 80;
992+
stream.isTTY = true;
993+
994+
let writtenContent = '';
995+
996+
// Override write to capture content
997+
const originalWrite = stream.write;
998+
stream.write = function (content) {
999+
writtenContent = content;
1000+
return originalWrite.call(this, content);
1001+
};
1002+
1003+
const spinner = ora({
1004+
stream,
1005+
text: Array.from({length: 10}, (_, i) => `Line ${i + 1}`).join('\n'), // 10 lines (exceeds 5 row height)
1006+
color: false,
1007+
isEnabled: true,
1008+
});
1009+
1010+
spinner.start();
1011+
spinner.render(); // Force a render
1012+
1013+
// When content exceeds viewport, should truncate with message
1014+
t.true(writtenContent.includes('Line 1'), 'Should include some original content');
1015+
t.true(writtenContent.includes('(content truncated to fit terminal)'), 'Should show truncation message');
1016+
1017+
// Should not include all 10 lines
1018+
const lineCount = (writtenContent.match(/Line \d+/g) || []).length;
1019+
t.true(lineCount < 10, 'Should truncate some lines');
1020+
t.true(lineCount <= 5, 'Should not exceed terminal height');
1021+
1022+
spinner.stop();
1023+
});
1024+
1025+
test('multiline text within console height', t => {
1026+
// Create a mock stream with sufficient height
1027+
const stream = getPassThroughStream();
1028+
stream.rows = 10; // Simulate a console with 10 rows
1029+
stream.columns = 80;
1030+
stream.isTTY = true;
1031+
1032+
let writtenContent = '';
1033+
1034+
// Override write to capture content
1035+
const originalWrite = stream.write;
1036+
stream.write = function (content) {
1037+
writtenContent = content;
1038+
return originalWrite.call(this, content);
1039+
};
1040+
1041+
const spinner = ora({
1042+
stream,
1043+
text: Array.from({length: 5}, (_, i) => `Line ${i + 1}`).join('\n'), // 5 lines (within 10 row height)
1044+
color: false,
1045+
isEnabled: true,
1046+
});
1047+
1048+
spinner.start();
1049+
spinner.render();
1050+
1051+
// When content is within viewport, should not truncate
1052+
t.true(writtenContent.includes('Line 1'), 'Should include first line');
1053+
t.true(writtenContent.includes('Line 5'), 'Should include last line');
1054+
t.false(writtenContent.includes('(content truncated to fit terminal)'), 'Should not show truncation message');
1055+
1056+
spinner.stop();
1057+
});
1058+
1059+
test('multiline text with undefined terminal rows', t => {
1060+
// Test fallback behavior when stream.rows is undefined
1061+
const stream = getPassThroughStream();
1062+
delete stream.rows; // Ensure rows is undefined
1063+
stream.columns = 80;
1064+
stream.isTTY = true;
1065+
1066+
let writtenContent = '';
1067+
1068+
// Override write to capture content
1069+
const originalWrite = stream.write;
1070+
stream.write = function (content) {
1071+
writtenContent = content;
1072+
return originalWrite.call(this, content);
1073+
};
1074+
1075+
const spinner = ora({
1076+
stream,
1077+
text: Array.from({length: 10}, (_, i) => `Line ${i + 1}`).join('\n'),
1078+
color: false,
1079+
isEnabled: true,
1080+
});
1081+
1082+
spinner.start();
1083+
spinner.render();
1084+
1085+
// When terminal height is unknown, should not truncate (no truncation applied)
1086+
t.true(writtenContent.includes('Line 1'), 'Should include first line');
1087+
t.true(writtenContent.includes('Line 10'), 'Should include last line');
1088+
t.false(writtenContent.includes('(content truncated to fit terminal)'), 'Should not truncate when height is unknown');
1089+
1090+
spinner.stop();
1091+
});
1092+
1093+
test('multiline text with very small console height', t => {
1094+
// Test edge case: console height = 1 (should not truncate since no room for message)
1095+
const stream = getPassThroughStream();
1096+
stream.rows = 1;
1097+
stream.columns = 80;
1098+
stream.isTTY = true;
1099+
1100+
let writtenContent = '';
1101+
const originalWrite = stream.write;
1102+
stream.write = function (content) {
1103+
writtenContent = content;
1104+
return originalWrite.call(this, content);
1105+
};
1106+
1107+
const spinner = ora({
1108+
stream,
1109+
text: 'Line 1\nLine 2\nLine 3', // 3 lines (exceeds 1 row height)
1110+
color: false,
1111+
isEnabled: true,
1112+
});
1113+
1114+
spinner.start();
1115+
spinner.render();
1116+
1117+
// When console is too small (1 row), should not truncate because no room for message
1118+
t.true(writtenContent.includes('Line 1'), 'Should include content');
1119+
t.false(writtenContent.includes('(content truncated to fit terminal)'), 'Should not truncate when console too small for message');
1120+
1121+
spinner.stop();
1122+
});

0 commit comments

Comments
 (0)