Skip to content

Commit

Permalink
Handle wrapping, add hideCursor option (#69)
Browse files Browse the repository at this point in the history
* Conform with latest XO

* Handle wrapped text

Count how many rows are taken up by the spinner and the text and clear
the appropriate number of lines. Fix clear() so it doesn't clear
*further* lines until render() is called again.

Fixes #38. Fixes #7.

* Add hideCursor option

Fixes #27. Users may still hide the cursor outside of Ora.

* Test with Node.js 8 and 9

* Properly clear empty lines

* Consistently set linesToClear to 0 once cleared
  • Loading branch information
novemberborn authored and sindresorhus committed Feb 21, 2018
1 parent 91dcdaa commit 4df5d19
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 7 deletions.
2 changes: 2 additions & 0 deletions .travis.yml
@@ -1,4 +1,6 @@
language: node_js
node_js:
- '9'
- '8'
- '6'
- '4'
51 changes: 46 additions & 5 deletions index.js
Expand Up @@ -3,6 +3,10 @@ const chalk = require('chalk');
const cliCursor = require('cli-cursor');
const cliSpinners = require('cli-spinners');
const logSymbols = require('log-symbols');
const stripAnsi = require('strip-ansi');
const wcwidth = require('wcwidth');

const TEXT = Symbol('text');

class Ora {
constructor(options) {
Expand All @@ -25,14 +29,31 @@ class Ora {
throw new Error('Spinner must define `frames`');
}

this.text = this.options.text;
this.color = this.options.color;
this.hideCursor = this.options.hideCursor !== false;
this.interval = this.options.interval || this.spinner.interval || 100;
this.stream = this.options.stream;
this.id = null;
this.frameIndex = 0;
this.enabled = typeof this.options.enabled === 'boolean' ? this.options.enabled : ((this.stream && this.stream.isTTY) && !process.env.CI);

// Set *after* this.stream
this.text = this.options.text;
this.linesToClear = 0;
}

get text() {
return this[TEXT];
}

set text(value) {
this[TEXT] = value;
const columns = this.stream.columns || 80;
this.lineCount = stripAnsi('--' + value).split('\n').reduce((count, line) => {
return count + Math.max(1, Math.ceil(wcwidth(line) / columns));
}, 0);
}

frame() {
const frames = this.spinner.frames;
let frame = frames[this.frameIndex];
Expand All @@ -45,22 +66,32 @@ class Ora {

return frame + ' ' + this.text;
}

clear() {
if (!this.enabled) {
return this;
}

this.stream.clearLine();
this.stream.cursorTo(0);
for (let i = 0; i < this.linesToClear; i++) {
if (i > 0) {
this.stream.moveCursor(0, -1);
}
this.stream.clearLine();
this.stream.cursorTo(0);
}
this.linesToClear = 0;

return this;
}

render() {
this.clear();
this.stream.write(this.frame());
this.linesToClear = this.lineCount;

return this;
}

start(text) {
if (text) {
this.text = text;
Expand All @@ -70,12 +101,15 @@ class Ora {
return this;
}

cliCursor.hide(this.stream);
if (this.hideCursor) {
cliCursor.hide(this.stream);
}
this.render();
this.id = setInterval(this.render.bind(this), this.interval);

return this;
}

stop() {
if (!this.enabled) {
return this;
Expand All @@ -85,22 +119,29 @@ class Ora {
this.id = null;
this.frameIndex = 0;
this.clear();
cliCursor.show(this.stream);
if (this.hideCursor) {
cliCursor.show(this.stream);
}

return this;
}

succeed(text) {
return this.stopAndPersist({symbol: logSymbols.success, text});
}

fail(text) {
return this.stopAndPersist({symbol: logSymbols.error, text});
}

warn(text) {
return this.stopAndPersist({symbol: logSymbols.warning, text});
}

info(text) {
return this.stopAndPersist({symbol: logSymbols.info, text});
}

stopAndPersist(options) {
if (!this.enabled) {
return this;
Expand Down
5 changes: 3 additions & 2 deletions package.json
Expand Up @@ -38,12 +38,13 @@
"chalk": "^2.1.0",
"cli-cursor": "^2.1.0",
"cli-spinners": "^1.0.1",
"log-symbols": "^2.1.0"
"log-symbols": "^2.1.0",
"strip-ansi": "^4.0.0",
"wcwidth": "^1.0.1"
},
"devDependencies": {
"ava": "*",
"get-stream": "^3.0.0",
"strip-ansi": "^3.0.1",
"xo": "*"
}
}
4 changes: 4 additions & 0 deletions readme.md
Expand Up @@ -72,6 +72,10 @@ Values: `black` `red` `green` `yellow` `blue` `magenta` `cyan` `white` `gray`

Color of the spinner.

##### hideCursor

Set to `false` to stop Ora from hiding the cursor.

##### interval

Type: `number`<br>
Expand Down
67 changes: 67 additions & 0 deletions test.js
Expand Up @@ -11,6 +11,7 @@ const getPassThroughStream = () => {
const stream = new PassThroughStream();
stream.clearLine = noop;
stream.cursorTo = noop;
stream.moveCursor = noop;
return stream;
};

Expand Down Expand Up @@ -181,3 +182,69 @@ test('.promise() - rejects', async t => {

t.regex(stripAnsi(await output), /(✖|×) foo/);
});

test('erases wrapped lines', t => {
const stream = getPassThroughStream();
stream.columns = 40;
let clearedLines = 0;
let cursorAtRow = 0;
stream.clearLine = () => {
clearedLines++;
};
stream.moveCursor = (dx, dy) => {
cursorAtRow += dy;
};
const reset = () => {
clearedLines = 0;
cursorAtRow = 0;
};

const spinner = new Ora({
stream,
text: 'foo',
color: false,
enabled: true
});

spinner.render();
t.is(clearedLines, 0);
t.is(cursorAtRow, 0);

spinner.text = 'foo\n\nbar';
spinner.render();
t.is(clearedLines, 1); // Cleared 'foo'
t.is(cursorAtRow, 0);

spinner.render();
t.is(clearedLines, 4); // Cleared 'foo\n\nbar'
t.is(cursorAtRow, -2);

spinner.clear();
reset();
spinner.text = '0'.repeat(stream.columns + 10);
spinner.render();
spinner.render();
t.is(clearedLines, 2);
t.is(cursorAtRow, -1);

spinner.clear();
reset();
// Unicorns take up two cells, so this creates 3 rows of text not two
spinner.text = '🦄'.repeat(stream.columns + 10);
spinner.render();
spinner.render();
t.is(clearedLines, 3);
t.is(cursorAtRow, -2);

spinner.clear();
reset();
// Unicorns take up two cells. Remove the spinner and space and fill two rows,
// then force a linebreak and write the third row.
spinner.text = '🦄'.repeat(stream.columns - 2) + '\nfoo';
spinner.render();
spinner.render();
t.is(clearedLines, 3);
t.is(cursorAtRow, -2);

spinner.stop();
});

0 comments on commit 4df5d19

Please sign in to comment.